Skip to main content
Advertisement

6.4 Template Literal Types

What Are Template Literal Types?

Introduced in TypeScript 4.1, template literal types lift JavaScript's template literals (`string ${variable}`) to the type level. They let you combine or transform string types to produce new string literal types.

type Greeting = `Hello, ${string}!`;
// Any string that starts with "Hello, " and ends with "!"

type EventName = `on${'Click' | 'Focus' | 'Blur'}`;
// 'onClick' | 'onFocus' | 'onBlur'

Template literal types let you:

  • Handle pattern-based strings — API paths, event names, CSS classes — in a type-safe way.
  • Automatically generate every combination of a union type.
  • Dynamically transform mapped type keys with the as clause.

Core Concepts

1. Basic Syntax

// Combine strings with types
type Prefix = 'get' | 'set' | 'delete';
type Resource = 'User' | 'Post' | 'Comment';

type ApiMethod = `${Prefix}${Resource}`;
// 'getUser' | 'getPost' | 'getComment'
// | 'setUser' | 'setPost' | 'setComment'
// | 'deleteUser' | 'deletePost' | 'deleteComment'

// Concrete type check
function callApi(method: ApiMethod): void {
console.log(`API call: ${method}`);
}

callApi('getUser'); // OK
callApi('setPost'); // OK
// callApi('fetchUser'); // Error!

2. Built-in String Utility Types

TypeScript provides four built-in utility types for string manipulation.

// Uppercase<S>: all characters to upper case
type Upper = Uppercase<'hello world'>; // 'HELLO WORLD'

// Lowercase<S>: all characters to lower case
type Lower = Lowercase<'Hello World'>; // 'hello world'

// Capitalize<S>: first character to upper case
type Cap = Capitalize<'helloWorld'>; // 'HelloWorld'

// Uncapitalize<S>: first character to lower case
type Uncap = Uncapitalize<'HelloWorld'>; // 'helloWorld'

// Combined with a type variable
type EventHandler<T extends string> = `on${Capitalize<T>}`;

type ClickHandler = EventHandler<'click'>; // 'onClick'
type FocusHandler = EventHandler<'focus'>; // 'onFocus'
type BlurHandler = EventHandler<'blur'>; // 'onBlur'

3. Union Combinations — Automatic Cartesian Product

When a union type appears inside a template literal type, every combination is generated automatically.

type Color = 'red' | 'green' | 'blue';
type Shade = 'light' | 'dark';

// 6 combinations are generated automatically
type ShadedColor = `${Shade}-${Color}`;
// 'light-red' | 'light-green' | 'light-blue'
// | 'dark-red' | 'dark-green' | 'dark-blue'

// CSS unit type
type CSSUnit = 'px' | 'rem' | 'em' | 'vw' | 'vh' | '%';
type CSSValue = `${number}${CSSUnit}`;

// Usage
function setWidth(value: CSSValue): void {
document.body.style.width = value;
}

setWidth('100px'); // OK
setWidth('2.5rem'); // OK
// setWidth('100pt'); // Error!

4. Event Type Automation Pattern

// Auto-generate event handler types with the on${EventName} pattern
type EventName = 'click' | 'focus' | 'blur' | 'change' | 'submit';

type EventHandlerName = `on${Capitalize<EventName>}`;
// 'onClick' | 'onFocus' | 'onBlur' | 'onChange' | 'onSubmit'

// Combined with a mapped type
interface DOMEventMap {
click: MouseEvent;
focus: FocusEvent;
blur: FocusEvent;
change: Event;
submit: SubmitEvent;
}

type DOMEventHandlers = {
[K in keyof DOMEventMap as `on${Capitalize<K & string>}`]?: (
event: DOMEventMap[K]
) => void;
};

// {
// onClick?: (event: MouseEvent) => void;
// onFocus?: (event: FocusEvent) => void;
// ...
// }

5. CSS Unit Types and API Path Types

// CSS units
type Length = `${number}${'px' | 'rem' | 'em' | 'vw' | 'vh'}`;
type Percentage = `${number}%`;
type CSSSize = Length | Percentage | 'auto' | 'inherit';

// Auto-generate directional CSS properties
type Direction = 'top' | 'right' | 'bottom' | 'left';
type SpacingProp = `${'margin' | 'padding'}-${Direction}`;
// 'margin-top' | 'margin-right' | ... | 'padding-left'

// REST API path types
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
type APIVersion = 'v1' | 'v2';
type ResourcePath = `/${APIVersion}/${'users' | 'posts' | 'comments'}`;
// '/v1/users' | '/v1/posts' | ... | '/v2/comments'

// Path parameter pattern
type IDParam = `${string}/:id`;
type ResourceWithId = `/${APIVersion}/${'users' | 'posts'}/:id`;

Code Examples

Example 1: Type-Safe Event Emitter

type EventMap = Record<string, unknown>;

class TypedEventEmitter<Events extends EventMap> {
private listeners = new Map<keyof Events, Set<Function>>();

on<K extends keyof Events & string>(
event: K,
listener: (data: Events[K]) => void
): this {
if (!this.listeners.has(event)) {
this.listeners.set(event, new Set());
}
this.listeners.get(event)!.add(listener);
return this;
}

off<K extends keyof Events & string>(
event: K,
listener: (data: Events[K]) => void
): this {
this.listeners.get(event)?.delete(listener);
return this;
}

emit<K extends keyof Events & string>(event: K, data: Events[K]): void {
this.listeners.get(event)?.forEach(listener => listener(data));
}

once<K extends keyof Events & string>(
event: K,
listener: (data: Events[K]) => void
): this {
const wrapper = (data: Events[K]) => {
listener(data);
this.off(event, wrapper);
};
return this.on(event, wrapper);
}
}

// Usage
interface AppEvents {
userLogin: { userId: number; timestamp: Date };
userLogout: { userId: number };
messageReceived: { from: number; content: string; roomId: string };
errorOccurred: { code: number; message: string };
}

const emitter = new TypedEventEmitter<AppEvents>();

emitter.on('userLogin', ({ userId, timestamp }) => {
console.log(`User ${userId} logged in: ${timestamp.toLocaleString()}`);
});

emitter.emit('userLogin', { userId: 42, timestamp: new Date() });
// emitter.emit('userLogin', { userId: '42' }); // Error! userId must be number

Example 2: Auto-Generate Redux Action Types

// Auto-generate types from slice name and action name
type ActionType<
Slice extends string,
Action extends string
> = `${Slice}/${Action}`;

// All action types for the users slice
type UsersActionTypes = ActionType<'users', 'fetch' | 'add' | 'remove' | 'update'>;
// 'users/fetch' | 'users/add' | 'users/remove' | 'users/update'

// Combined with a payload map
interface ActionPayloadMap {
'users/fetch': undefined;
'users/add': { name: string; email: string };
'users/remove': { id: number };
'users/update': { id: number; data: Partial<{ name: string; email: string }> };
'posts/fetch': { userId?: number };
'posts/add': { title: string; content: string; userId: number };
}

type ActionOf<T extends keyof ActionPayloadMap> =
ActionPayloadMap[T] extends undefined
? { type: T }
: { type: T; payload: ActionPayloadMap[T] };

// Usage
type FetchUsersAction = ActionOf<'users/fetch'>;
// { type: 'users/fetch' }

type AddUserAction = ActionOf<'users/add'>;
// { type: 'users/add'; payload: { name: string; email: string } }

// Dispatch function type
function dispatch<T extends keyof ActionPayloadMap>(
action: ActionOf<T>
): void {
console.log('dispatch:', action);
}

dispatch({ type: 'users/add', payload: { name: 'Alice', email: 'alice@example.com' } });
dispatch({ type: 'users/fetch' });

Example 3: i18n Key Types

// Translation key structure
interface Translations {
common: {
save: string;
cancel: string;
delete: string;
confirm: string;
};
user: {
profile: {
title: string;
editButton: string;
};
settings: {
language: string;
theme: string;
};
};
error: {
notFound: string;
unauthorized: string;
serverError: string;
};
}

// Extract nested keys as dot-separated paths
type DotPath<T, Prefix extends string = ''> = {
[K in keyof T & string]: T[K] extends string
? Prefix extends ''
? K
: `${Prefix}.${K}`
: DotPath<T[K], Prefix extends '' ? K : `${Prefix}.${K}`>;
}[keyof T & string];

type TranslationKey = DotPath<Translations>;
// 'common.save' | 'common.cancel' | 'common.delete' | 'common.confirm'
// | 'user.profile.title' | 'user.profile.editButton'
// | 'user.settings.language' | 'user.settings.theme'
// | 'error.notFound' | 'error.unauthorized' | 'error.serverError'

// Type-safe translation function
function t(key: TranslationKey): string {
// In a real implementation, look up the key in a translation dictionary
return key;
}

t('common.save'); // OK
t('user.profile.title'); // OK
// t('common.unknown'); // Error!
// t('user.profile'); // Error! Not a leaf node

Example 4: Auto-Typed API Client

// Auto-generate REST API endpoint types
type CRUDAction = 'list' | 'get' | 'create' | 'update' | 'delete';

type EndpointName<
Resource extends string,
Action extends CRUDAction
> = `${Lowercase<Resource>}${Capitalize<Action>}`;

type UserEndpoints = EndpointName<'User', CRUDAction>;
// 'userList' | 'userGet' | 'userCreate' | 'userUpdate' | 'userDelete'

// HTTP method mapping
type HTTPMethodFor<Action extends CRUDAction> =
Action extends 'list' | 'get' ? 'GET' :
Action extends 'create' ? 'POST' :
Action extends 'update' ? 'PUT' :
Action extends 'delete' ? 'DELETE' :
never;

// Type-safe fetch wrapper
type FetchOptions<Method extends string> = {
method: Method;
headers?: Record<string, string>;
body?: Method extends 'GET' ? never : string;
};

Pro Tips

Tip 1: Parsing Complex Strings — Recursion + infer

// Extract parameter names from a path string
// "/users/:id/posts/:postId" -> 'id' | 'postId'
type ExtractRouteParams<Path extends string> =
Path extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractRouteParams<`/${Rest}`>
: Path extends `${string}:${infer Param}`
? Param
: never;

type Params = ExtractRouteParams<'/users/:id/posts/:postId/comments/:commentId'>;
// 'id' | 'postId' | 'commentId'

// Build a params object type from the path
type RouteParams<Path extends string> = {
[K in ExtractRouteParams<Path>]: string;
};

type UserPostParams = RouteParams<'/users/:userId/posts/:postId'>;
// { userId: string; postId: string; }

// Usage
function navigate<Path extends string>(
path: Path,
params: RouteParams<Path>
): string {
let result: string = path;
for (const [key, value] of Object.entries(params)) {
result = result.replace(`:${key}`, value);
}
return result;
}

const url = navigate('/users/:userId/posts/:postId', {
userId: '42',
postId: '123',
});
// '/users/42/posts/123'

Tip 2: Parsing Query String Types

// Extract keys from a "key1=value1&key2=value2" string
type ParseQueryKey<Q extends string> =
Q extends `${infer Key}=${string}&${infer Rest}`
? Key | ParseQueryKey<Rest>
: Q extends `${infer Key}=${string}`
? Key
: never;

type Keys = ParseQueryKey<'name=john&age=30&role=admin'>;
// 'name' | 'age' | 'role'

Tip 3: Performance Considerations

// Warning: type computation cost grows sharply as the union size increases

// Poor example: too many combinations
type TooMany =
`${string & ('a' | 'b' | 'c' | 'd' | 'e' | 'f' | 'g' | 'h')}${
string & ('1' | '2' | '3' | '4' | '5' | '6' | '7' | '8')}`;
// 64 combinations — heavy compiler load

// Recommended: define only what you need
type Header = 'X-Request-Id' | 'X-Correlation-Id' | 'Authorization' | 'Content-Type';
// An explicit union is faster to compile than a template literal

// Recursion depth is also limited (TypeScript default: ~100)
// For deep recursion, split into stages

// Acceptable range
type Digit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
type TwoDigit = `${Digit}${Digit}`; // 100 combinations — within limits

Tip 4: Using Type Information at Runtime

// Pattern for managing types and runtime values together
const HTTP_METHODS = ['GET', 'POST', 'PUT', 'DELETE', 'PATCH'] as const;
type HTTPMethod = typeof HTTP_METHODS[number];

// Runtime check + type guard
function isHTTPMethod(value: string): value is HTTPMethod {
return (HTTP_METHODS as readonly string[]).includes(value);
}

// Use an event name array as a type source
const WIDGET_EVENTS = ['click', 'hover', 'focus', 'blur'] as const;
type WidgetEvent = typeof WIDGET_EVENTS[number];
type WidgetEventHandler = `on${Capitalize<WidgetEvent>}`;
// 'onClick' | 'onHover' | 'onFocus' | 'onBlur'

const WIDGET_EVENT_HANDLERS: WidgetEventHandler[] = WIDGET_EVENTS.map(
event => `on${event.charAt(0).toUpperCase()}${event.slice(1)}` as WidgetEventHandler
);

Summary Table

FeatureSyntax ExampleDescription
Basic combination`${A}${B}`Combine two types into a string
Union Combination`${'a' &#124; 'b'}X`Generates all combinations of unions
UppercaseUppercase<'hello'>All characters to upper case
LowercaseLowercase<'HELLO'>All characters to lower case
CapitalizeCapitalize<'hello'>First character to upper case
UncapitalizeUncapitalize<'Hello'>First character to lower case
Event handler`on${Capitalize<T>}`Generate handler type from event name
Route param extractioninfer + recursionDecompose patterns from a string
i18n keysDotPath<T> patternExtract nested object keys as dot paths
CSS Value`${number}${'px' &#124; 'rem'}`Number + unit string type
API paths`/${Version}/${Resource}`Type-safe REST path strings

Next we will look at Recursive Types (6.5). You will model hierarchical data — JSON structures, file system trees, nested menus — using self-referencing types, and implement advanced utility types like DeepPartial and DeepReadonly yourself.

Advertisement