Skip to main content

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:

typescript
type Greeting = `Hello, ${string}`;

This creates a type that matches any string that starts with "Hello, " followed by any string content.

Example: Basic Usage

typescript
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:

typescript
type Animal = 'cat' | 'dog' | 'bird';
type Sound = 'meow' | 'woof' | 'chirp';

type AnimalSound = `${Animal} goes ${Sound}`;

The AnimalSound type is equivalent to:

typescript
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

typescript
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 uppercase
  • Lowercase<T>: Converts string literal type to lowercase
  • Capitalize<T>: Capitalizes first character of string literal type
  • Uncapitalize<T>: Converts first character of string literal type to lowercase

Example: Case Transformation

typescript
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:

typescript
// 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:

typescript
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:

typescript
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:

typescript
// 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:

typescript
// 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:

  1. Create complex string-based types by combining string literals with other types
  2. Generate unions of string combinations automatically
  3. Transform string types using built-in utility types like Uppercase, Lowercase, etc.
  4. Extract and infer parts of string patterns for advanced type manipulation
  5. 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

Exercises

  1. Create a type that represents valid CSS color values (hex, rgb, rgba, named colors)
  2. Build a router type that extracts parameters from URL patterns
  3. Design a type-safe event emitter using template literal types
  4. Create a validation schema type using template literal types to enforce property naming rules
  5. 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! :)