TypeScript Template Literal Types
Introduction
Template Literal Types, introduced in TypeScript 4.1, bring the power of JavaScript's template literals to the type system. This feature allows you to create complex string-based types by combining string literals with other types, enabling pattern matching, string manipulation, and type inference at the type level.
Template literal types are particularly useful when working with string-based APIs, creating strongly typed event systems, or building type-safe utilities for string transformation.
Understanding Template Literal Types
Basic Syntax
Template literal types use the same syntax as JavaScript's template literals (backticks), but at the type level:
type Greeting = `Hello, ${string}`;
This creates a type that matches any string that starts with "Hello, " followed by any string content.
Example: Basic Usage
type Greeting = `Hello, ${string}`;
// Valid - matches the pattern
const greeting1: Greeting = "Hello, World"; // ✅
// Invalid - doesn't match the pattern
const greeting2: Greeting = "Hi there, World"; // ❌ Type '"Hi there, World"' is not assignable to type '`Hello, ${string}`'
Combining with Union Types
Template literal types become even more powerful when combined with union types:
type Animal = 'cat' | 'dog' | 'bird';
type Sound = 'meow' | 'woof' | 'chirp';
type AnimalSound = `${Animal} goes ${Sound}`;
The AnimalSound
type is equivalent to:
type AnimalSound =
| 'cat goes meow' | 'cat goes woof' | 'cat goes chirp'
| 'dog goes meow' | 'dog goes woof' | 'dog goes chirp'
| 'bird goes meow' | 'bird goes woof' | 'bird goes chirp';
TypeScript automatically creates all possible combinations of the union types in the template.
Example: Event Handling System
type EventType = 'click' | 'change' | 'mouseover';
type ElementType = 'button' | 'input' | 'div';
// Creates types like 'button:click', 'input:mouseover', etc.
type UIEvent = `${ElementType}:${EventType}`;
// Event handler function
function handleEvent(event: UIEvent, data: any) {
const [element, eventType] = event.split(':');
console.log(`Handling ${eventType} event on ${element} with data:`, data);
}
// Valid usage
handleEvent('button:click', { x: 100, y: 200 }); // ✅
handleEvent('input:change', { value: 'New text' }); // ✅
// Type error - 'focus' is not in EventType
// handleEvent('button:focus', {}); // ❌
String Manipulation with Intrinsic Types
TypeScript provides several intrinsic type operators that work with template literal types:
Uppercase<T>
: Converts string literal type to uppercaseLowercase<T>
: Converts string literal type to lowercaseCapitalize<T>
: Capitalizes first character of string literal typeUncapitalize<T>
: Converts first character of string literal type to lowercase
Example: Case Transformation
type Greeting = "hello, world";
type UppercaseGreeting = Uppercase<Greeting>; // "HELLO, WORLD"
type LowercaseGreeting = Lowercase<"HELLO, WORLD">; // "hello, world"
type CapitalizedGreeting = Capitalize<Greeting>; // "Hello, world"
type UncapitalizedGreeting = Uncapitalize<"Hello, world">; // "hello, world"
Practical Applications
Creating Type-Safe APIs
Template literal types are excellent for building type-safe APIs, especially for routing and validation:
// Define API endpoints with path parameters
type ApiEndpoint =
| `/users/${string}`
| `/users/${string}/posts`
| `/products/${number}`;
function fetchFromApi(endpoint: ApiEndpoint) {
// Implementation...
console.log(`Fetching data from: ${endpoint}`);
}
// Valid endpoints
fetchFromApi('/users/123'); // ✅
fetchFromApi('/users/john/posts'); // ✅
fetchFromApi('/products/42'); // ✅
// Invalid endpoints
// fetchFromApi('/orders/123'); // ❌ Argument of type '"/orders/123"' is not assignable to parameter of type 'ApiEndpoint'
Strongly Typed CSS-in-JS
Template literal types can provide type safety in CSS-in-JS libraries:
type CSSProperty = 'color' | 'background' | 'margin' | 'padding';
type CSSValue = string | number;
type CSSUnit = 'px' | 'em' | 'rem' | '%';
type CSSMeasurement = `${number}${CSSUnit}`;
type CSSDefinition = `${CSSProperty}: ${CSSValue | CSSMeasurement}`;
function css(...styles: CSSDefinition[]): string {
return styles.join('; ');
}
// Valid CSS definitions
const style = css(
'color: red',
'margin: 10px',
'padding: 5rem'
); // ✅
// Invalid CSS definitions
// const invalidStyle = css('width: wide'); // ❌ 'width' is not a valid CSSProperty
Event Handler Naming Conventions
Create type-safe event handler naming conventions for component props:
type EventName = 'change' | 'click' | 'focus' | 'blur';
type HandlerName = `on${Capitalize<EventName>}`;
// Results in: 'onChange' | 'onClick' | 'onFocus' | 'onBlur'
interface ButtonProps {
label: string;
onClick?: () => void;
onFocus?: () => void;
// More event handlers...
}
// Type helper to ensure all handler names follow convention
type ComponentProps<T extends Record<string, any>> = {
[K in keyof T]: K extends HandlerName ? (...args: any[]) => void : T[K];
};
// Now TypeScript will enforce our naming convention
const Button = (props: ComponentProps<ButtonProps>) => {
// Implementation...
return <button>{props.label}</button>;
};
Advanced Pattern Matching with Inference
Template literal types can be used with conditional types to infer parts of strings:
// Extract route parameters from a path
type ExtractRouteParams<T extends string> =
T extends `${string}/:${infer Param}/${string}` ? Param :
T extends `${string}/:${infer Param}` ? Param :
never;
// Examples
type UserRouteParams = ExtractRouteParams<'/users/:userId'>; // "userId"
type PostRouteParams = ExtractRouteParams<'/users/:userId/posts/:postId'>; // "userId"
// More advanced router type
type RouteParams<Route extends string> = {
[K in Route extends `${string}/:${infer Param}/${infer Rest}`
? Param | Extract<`${Rest}`, `${infer ParamRest}/${string}` | `${infer ParamRest}`>
: Route extends `${string}/:${infer Param}`
? Param
: never]: string;
};
// Usage
type UserPostParams = RouteParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string; }
function createRouter<Route extends string>(route: Route, callback: (params: RouteParams<Route>) => void) {
// Router implementation...
}
createRouter('/users/:userId/posts/:postId', (params) => {
// params is typed as { userId: string; postId: string; }
console.log(params.userId, params.postId);
});
Real-World Use Case: Typed Redux Action Creators
Template literal types shine when creating typed Redux action creators:
// Define action types using template literals
type EntityType = 'user' | 'post' | 'comment';
type ActionType = 'fetch' | 'create' | 'update' | 'delete';
type ActionName = `${ActionType}_${EntityType}`;
type AsyncSuffix = 'request' | 'success' | 'failure';
type AsyncActionName = `${ActionName}_${AsyncSuffix}`;
// All possible action types:
// 'fetch_user_request' | 'fetch_user_success' | ...and so on
// Action creator factory
function createAsyncActionCreator<E extends EntityType, A extends ActionType>(
entityType: E,
actionType: A
) {
type PayloadType = Record<string, any>;
return {
request: (requestPayload?: PayloadType) => ({
type: `${actionType}_${entityType}_request` as const,
payload: requestPayload
}),
success: (responsePayload: PayloadType) => ({
type: `${actionType}_${entityType}_success` as const,
payload: responsePayload
}),
failure: (error: Error) => ({
type: `${actionType}_${entityType}_failure` as const,
error
})
};
}
// Usage
const fetchUserActions = createAsyncActionCreator('user', 'fetch');
// Dispatch with type safety
dispatch(fetchUserActions.request({ id: 123 }));
dispatch(fetchUserActions.success({ id: 123, name: 'John' }));
dispatch(fetchUserActions.failure(new Error('Failed to fetch user')));
Summary
Template literal types represent a powerful addition to TypeScript's type system, allowing you to:
- Create complex string-based types by combining string literals with other types
- Generate unions of string combinations automatically
- Transform string types using built-in utility types like
Uppercase
,Lowercase
, etc. - Extract and infer parts of string patterns for advanced type manipulation
- Build type-safe string-based APIs and interfaces
They excel in scenarios involving string-based APIs, event systems, routing mechanisms, and naming conventions. By leveraging template literal types, you can catch errors at compile time rather than runtime, resulting in more robust and maintainable code.
Additional Resources
- TypeScript Documentation: Template Literal Types
- TypeScript Playground - Try out template literal types interactively
- TypeScript Release Notes 4.1 - Original release notes for template literal types
Exercises
- Create a type that represents valid CSS color values (hex, rgb, rgba, named colors)
- Build a router type that extracts parameters from URL patterns
- Design a type-safe event emitter using template literal types
- Create a validation schema type using template literal types to enforce property naming rules
- Build a type-safe query builder for a SQL-like API using template literal types
By mastering template literal types, you'll be able to build more type-safe and self-documenting APIs in your TypeScript applications!
If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)