TypeScript Template Literal Types for String Manipulation
Master template literal types to build type-safe string patterns for routes, events, CSS values, and API endpoints in TypeScript.
What you'll learn
- ✓Build string pattern types with template literals
- ✓Use intrinsic string manipulation types like Uppercase and Capitalize
- ✓Create type-safe event emitters and route handlers
Prerequisites
- •Basic TypeScript generics
- •Union types and literal types
Template literal types, introduced in TypeScript 4.1, let you manipulate strings at the type level. They use the same backtick syntax as JavaScript template literals but operate entirely in the type system. Combined with union distribution and conditional types, they unlock patterns that were previously impossible without code generation.
Basic Syntax
A template literal type interpolates other types into a string pattern.
type Greeting = `Hello, ${string}`;
const a: Greeting = 'Hello, World'; // valid
const b: Greeting = 'Goodbye, World'; // error
You can interpolate unions, and TypeScript distributes across them to produce every combination.
type Size = 'sm' | 'md' | 'lg';
type Color = 'red' | 'blue';
type ClassName = `${Size}-${Color}`;
// 'sm-red' | 'sm-blue' | 'md-red' | 'md-blue' | 'lg-red' | 'lg-blue'
This distribution is the key mechanism that makes template literal types powerful.
Intrinsic String Manipulation Types
TypeScript provides four built-in utility types for transforming string literals.
type A = Uppercase<'hello'>; // 'HELLO'
type B = Lowercase<'HELLO'>; // 'hello'
type C = Capitalize<'hello'>; // 'Hello'
type D = Uncapitalize<'Hello'>; // 'hello'
These are compiler intrinsics, not user-defined conditional types. They work on literal string types and distribute over unions.
type Events = 'click' | 'focus' | 'blur';
type HandlerNames = `on${Capitalize<Events>}`;
// 'onClick' | 'onFocus' | 'onBlur'
Type-Safe Event Emitter
Template literal types let you build an event emitter where the handler signature is inferred from the event name.
type EventMap = {
click: { x: number; y: number };
focus: { target: string };
resize: { width: number; height: number };
};
type EventHandler<T> = (payload: T) => void;
class TypedEmitter<E extends Record<string, unknown>> {
private handlers = new Map<string, Function[]>();
on<K extends string & keyof E>(
event: K,
handler: EventHandler<E[K]>
): void {
const list = this.handlers.get(event) ?? [];
list.push(handler);
this.handlers.set(event, list);
}
emit<K extends string & keyof E>(event: K, payload: E[K]): void {
const list = this.handlers.get(event) ?? [];
list.forEach((fn) => fn(payload));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on('click', (payload) => {
// payload is { x: number; y: number }
console.log(payload.x, payload.y);
});
emitter.on('resize', (payload) => {
// payload is { width: number; height: number }
console.log(payload.width);
});
Route Pattern Matching
You can extract dynamic segments from route strings using template literal types and conditional type inference.
type ExtractParams<T extends string> =
T extends `${infer _Start}:${infer Param}/${infer Rest}`
? Param | ExtractParams<Rest>
: T extends `${infer _Start}:${infer Param}`
? Param
: never;
type UserRoute = '/users/:userId/posts/:postId';
type Params = ExtractParams<UserRoute>;
// 'userId' | 'postId'
type RouteParams<T extends string> = {
[K in ExtractParams<T>]: string;
};
function navigate<T extends string>(
route: T,
params: RouteParams<T>
): string {
let result: string = route;
for (const [key, value] of Object.entries(params)) {
result = result.replace(`:${key}`, value as string);
}
return result;
}
// TypeScript enforces that both params are provided
navigate('/users/:userId/posts/:postId', {
userId: '123',
postId: '456',
});
CSS Value Types
Template literal types can model CSS values precisely.
type CSSUnit = 'px' | 'rem' | 'em' | '%' | 'vh' | 'vw';
type CSSLength = `${number}${CSSUnit}`;
function setWidth(el: HTMLElement, width: CSSLength) {
el.style.width = width;
}
setWidth(document.body, '100px'); // valid
setWidth(document.body, '2.5rem'); // valid
setWidth(document.body, 'wide'); // error
You can also model compound CSS values.
type CSSColor = `#${string}` | `rgb(${number}, ${number}, ${number})`;
type Padding = CSSLength | `${CSSLength} ${CSSLength}`;
const p1: Padding = '10px'; // valid
const p2: Padding = '10px 20px'; // valid
Building API Endpoint Types
type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'posts' | 'comments';
type Endpoint = `/api/${ApiVersion}/${Resource}`;
// '/api/v1/users' | '/api/v1/posts' | '/api/v1/comments'
// | '/api/v2/users' | '/api/v2/posts' | '/api/v2/comments'
async function fetchResource(endpoint: Endpoint): Promise<unknown> {
const response = await fetch(endpoint);
return response.json();
}
fetchResource('/api/v1/users'); // valid
fetchResource('/api/v3/users'); // error
Splitting and Joining at the Type Level
You can implement Split and Join as recursive conditional types.
type Split<S extends string, D extends string> =
S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S];
type Parts = Split<'a.b.c', '.'>;
// ['a', 'b', 'c']
type Join<T extends string[], D extends string> =
T extends [infer Head extends string]
? Head
: T extends [infer Head extends string, ...infer Rest extends string[]]
? `${Head}${D}${Join<Rest, D>}`
: '';
type Joined = Join<['hello', 'world'], '-'>;
// 'hello-world'
Dot-Notation Path Types
A common real-world use is typing deeply nested object access with dot notation.
type Primitive = string | number | boolean | null | undefined;
type PathKeys<T> = T extends Primitive
? never
: {
[K in keyof T & string]: T[K] extends Primitive
? K
: K | `${K}.${PathKeys<T[K]>}`;
}[keyof T & string];
interface AppConfig {
db: {
host: string;
port: number;
credentials: {
user: string;
password: string;
};
};
cache: {
ttl: number;
enabled: boolean;
};
}
type ConfigPath = PathKeys<AppConfig>;
// 'db' | 'db.host' | 'db.port' | 'db.credentials'
// | 'db.credentials.user' | 'db.credentials.password'
// | 'cache' | 'cache.ttl' | 'cache.enabled'
function getConfig(path: ConfigPath): unknown {
// implementation
return undefined;
}
getConfig('db.host'); // valid
getConfig('db.credentials.user'); // valid
getConfig('db.missing'); // error
Performance Considerations
Template literal types that distribute over large unions can produce a combinatorial explosion. If you have a union with 100 members and combine two of them, TypeScript evaluates 10,000 combinations. Keep your unions small or use branded string types instead when the combination count grows.
// This is fine: 3 x 3 = 9 combinations
type Small = `${'a' | 'b' | 'c'}-${'x' | 'y' | 'z'}`;
// Avoid: large unions produce slow compilation
// type Big = `${HundredStrings}-${HundredStrings}`; // 10,000 types
Wrap Up
Template literal types bring string manipulation into the type system. Use them for event names, route patterns, CSS values, API endpoints, and any string-shaped contract you want the compiler to enforce. Combine them with conditional types and infer for parsing and transformation. Keep union sizes manageable to avoid slow builds.
Related articles
- TypeScript TypeScript Generics: Advanced Patterns and Constraints
Master advanced TypeScript generics patterns including conditional constraints, recursive types, generic factories, and type-safe builder patterns with examples.
- TypeScript Advanced Conditional Types With infer in TypeScript
Go beyond basic conditional types with infer, recursive conditionals, and distributive patterns to build powerful type-level programs.
- TypeScript TypeScript Generics Deep Dive
Go beyond basic generics — constraints, conditional types, mapped types, infer, and real-world patterns for writing type-safe reusable code.
- TypeScript TypeScript Recursive Types Tutorial
Build recursive types in TypeScript: deep readonly, JSON, paths, and tuple manipulation. Learn how to write recursion that the compiler can actually evaluate.