← Back
April 4, 2026NodeJS6 min read
TypeScript template literal types: type-safe string manipulation
Published April 4, 20266 min read
I was building an event system where event names followed a pattern like "user:created", "order:cancelled", "payment:failed". I wanted type safety on these strings without manually listing every combination. Template literal types made it possible to derive the full union automatically from the component parts. Here is the pattern and the real-world use cases that make it worth learning.
Basic template literal types
typescript
type Noun = 'user' | 'order' | 'payment';
type Verb = 'created' | 'updated' | 'deleted' | 'failed';
// Generates all combinations: "user:created", "user:updated", ..., "payment:failed"
type EventName = `${Noun}:${Verb}`;
// Type-safe event system
type EventHandler = (data: T) => void;
type EventMap = Record;
function on(event: EventName, handler: EventHandler): void {
// ...
}
on("user:created", handleUserCreated); // OK
on("payment:failed", handlePaymentFail); // OK
on("user:launched", handler); // Error! "launched" not in Verb
CSS property generation
typescript
type Side = 'top' | 'right' | 'bottom' | 'left';
type Size = 'sm' | 'md' | 'lg' | 'xl';
// Tailwind-like spacing classes
type SpacingClass = `m${'t' | 'r' | 'b' | 'l' | 'x' | 'y'}-${Size}`;
// "mt-sm" | "mt-md" | "mt-lg" | "mt-xl" | "mr-sm" | ...
type PaddingClass = `p${'t' | 'r' | 'b' | 'l' | 'x' | 'y'}-${Size}`;
type ClassName = SpacingClass | PaddingClass | 'flex' | 'block' | 'hidden';
function cx(...classes: ClassName[]): string {
return classes.join(' ');
}
cx('mt-lg', 'px-md', 'flex'); // Type-safe
cx('mt-huge'); // Error! "huge" not in Size
Automatic getter/setter method names
typescript
type Getter = `get${Capitalize}`;
type Setter = `set${Capitalize}`;
type ModelProps = 'name' | 'email' | 'role';
// Generates: { getName, setName, getEmail, setEmail, getRole, setRole }
type ModelMethods = {
[K in ModelProps as Getter]: () => string;
} & {
[K in ModelProps as Setter]: (value: string) => void;
};
function createModel(): ModelMethods {
const state: Record = {};
return new Proxy({} as ModelMethods, {
get(_, prop: string) {
if (prop.startsWith('get')) {
const key = prop.slice(3).toLowerCase();
return () => state[key];
}
if (prop.startsWith('set')) {
const key = prop.slice(3).toLowerCase();
return (value: string) => { state[key] = value; };
}
}
});
}
const model = createModel();
model.setName("Alice");
model.getName(); // "Alice"
model.setFoo("x"); // Error! setFoo not in ModelMethods
Type-safe API client paths
typescript
type ApiVersion = 'v1' | 'v2';
type Resource = 'users' | 'orders' | 'products';
type ApiPath = `/api/${ApiVersion}/${Resource}`;
// "/api/v1/users" | "/api/v1/orders" | "/api/v1/products" | "/api/v2/..."
// More complex: with ID params
type ResourcePath = `/api/${ApiVersion}/${Resource}` | `/api/${ApiVersion}/${Resource}/${string}`;
async function apiCall(path: ResourcePath, options?: RequestInit): Promise {
const response = await fetch(path, options);
return response.json();
}
apiCall('/api/v1/users');
apiCall('/api/v2/orders/123');
apiCall('/api/v3/users'); // Error! v3 not in ApiVersion
String manipulation utilities
typescript
// Built-in template literal type utilities
type Upper = Uppercase<'hello world'>; // "HELLO WORLD"
type Lower = Lowercase<'HELLO WORLD'>; // "hello world"
type Camel = Capitalize<'hello world'>; // "Hello world"
type Uncamel = Uncapitalize<'HelloWorld'>; // "helloWorld"
// Useful for code generation
type CamelCase =
S extends `${infer Head}_${infer Tail}`
? `${Head}${Capitalize>}`
: S;
type DBColumn = 'user_id' | 'created_at' | 'order_count';
type CamelColumns = { [K in DBColumn as CamelCase]: string };
// { userId: string; createdAt: string; orderCount: string }
Template literal types are one of those TypeScript features that look obscure until you hit the right use case, and then they feel essential. Event systems, CSS class utilities, API paths, and code generation patterns all benefit from the ability to derive exact string unions from component types rather than manually maintaining long union lists.
Share this
← All Posts6 min read