Type-Level Programming
TypeScript's type system is Turing-complete. Beyond describing the shapes of values, you can write programs that run entirely at compile time: computing new types from existing types using conditional types, mapped types, template literal types, and recursive type operations.
This page walks through the key primitives and combines them into real-world patterns.
The Building Blocks
Type-level programming relies on four core features:
- Conditional types —
T extends U ? A : B - Mapped types —
{ [K in keyof T]: ... } - Template literal types —
\prefix_${T}`` - Infer — extracting types from patterns
Let us look at each in depth.
Conditional Types
Conditional types are the if/else of the type system:
type IsString<T> = T extends string ? true : false; type A = IsString<string>; // true type B = IsString<number>; // false type C = IsString<"hello">; // true (string literal extends string)
Conditional types distribute over union types by default when T is a bare type parameter:
type ToArray<T> = T extends unknown ? T[] : never; // Distribution: each member of the union is processed separately type Result = ToArray<string | number>; // Equivalent to: string[] | number[] // NOT (string | number)[] // To prevent distribution, wrap in a tuple type ToArrayNoDistrib<T> = [T] extends [unknown] ? T[] : never; type Result2 = ToArrayNoDistrib<string | number>; // (string | number)[]
T is a naked type parameter — not when it is wrapped in a tuple, object, or another generic.The infer Keyword
infer lets you extract sub-types from a pattern during a conditional type check. Think of it as pattern matching for types:
// Extract the return type of a function type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never; type Fn = (x: number) => string; type R = ReturnType<Fn>; // string // Extract the element type of a Promise type Awaited<T> = T extends Promise<infer R> ? Awaited<R> : T; type P = Awaited<Promise<Promise<number>>>; // number // Extract parameters as a tuple type Parameters<T> = T extends (...args: infer P) => any ? P : never; type Params = Parameters<(a: string, b: number) => void>; // [string, number]
// Extract the first element of a tuple type Head<T extends readonly unknown[]> = T extends readonly [infer First, ...unknown[]] ? First : never; type H = Head<[string, number, boolean]>; // string // Extract everything except the first element type Tail<T extends readonly unknown[]> = T extends readonly [unknown, ...infer Rest] ? Rest : never; type T2 = Tail<[string, number, boolean]>; // [number, boolean]
Mapped Types
Mapped types transform every property in an object type. They are the Array.map of object types:
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Partial<T> = {
[K in keyof T]?: T[K];
};
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
interface User {
id: number;
name: string;
email: string;
}
type PartialUser = Partial<User>; // all optional
type NullableUser = Nullable<User>; // all nullable
type ReadonlyUser = Readonly<User>; // all readonlyYou can also remap keys with as and filter properties:
// Add "get" prefix to every key
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface User { id: number; name: string; }
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; }
// Filter: keep only string-valued properties
type StringProps<T> = {
[K in keyof T as T[K] extends string ? K : never]: T[K];
};
interface Mixed { id: number; name: string; active: boolean; role: string; }
type OnlyStrings = StringProps<Mixed>;
// { name: string; role: string; }Template Literal Types
Template literal types let you construct string literal types programmatically:
type EventName<T extends string> = `on${Capitalize<T>}`;
type MouseEvents = EventName<'click' | 'hover' | 'focus'>;
// 'onClick' | 'onHover' | 'onFocus'
// Build CSS property names
type CSSPropertyX = `margin-${string}` | `padding-${string}`;
// Extract the event name from a handler key
type StripOn<T extends string> = T extends `on${infer Event}` ? Uncapitalize<Event> : never;
type Events = StripOn<'onClick' | 'onHover'>; // 'click' | 'hover'Recursive Types
Type-level programs can recurse, enabling deep transformations:
// Deep readonly — makes every nested object readonly
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
interface Config {
server: { host: string; port: number; };
db: { url: string; poolSize: number; };
}
type FrozenConfig = DeepReadonly<Config>;
// server.host becomes readonly — even nested!
// Deep partial
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
type PartialConfig = DeepPartial<Config>;
// { server?: { host?: string; port?: number } }// Flatten a nested array type
type Flatten<T> = T extends Array<infer Item>
? Item extends Array<unknown>
? Flatten<Item>
: Item
: T;
type Nested = Flatten<number[][][]>; // number
type Shallow = Flatten<string[]>; // string
type NotArray = Flatten<boolean>; // booleanBuilding a Type-Safe API Client
Here is a real-world example that combines all the primitives to derive a typed HTTP client from a route definition:
interface Routes {
'GET /users': { response: User[] };
'GET /users/:id': { params: { id: string }; response: User };
'POST /users': { body: CreateUserDto; response: User };
'DELETE /users/:id': { params: { id: string }; response: void };
}
type Method = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
// Extract all routes for a given method
type RoutesForMethod<M extends Method> = {
[K in keyof Routes as K extends `${M} ${string}` ? K : never]: Routes[K];
};
type GetRoutes = RoutesForMethod<'GET'>;
// { 'GET /users': {...}; 'GET /users/:id': {...} }
// Extract the response type from a route key
type ResponseOf<K extends keyof Routes> =
Routes[K] extends { response: infer R } ? R : never;
type UsersResponse = ResponseOf<'GET /users'>; // User[]Type-Safe Event Emitter
type EventMap = {
login: { userId: string; timestamp: Date };
logout: { userId: string };
message: { from: string; text: string; roomId: number };
};
type EventKey = keyof EventMap;
class TypedEmitter<Events extends Record<string, unknown>> {
private listeners: Partial<{
[K in keyof Events]: Array<(data: Events[K]) => void>;
}> = {};
on<K extends keyof Events>(event: K, handler: (data: Events[K]) => void): void {
const arr = (this.listeners[event] ??= []) as Array<(data: Events[K]) => void>;
arr.push(handler);
}
emit<K extends keyof Events>(event: K, data: Events[K]): void {
this.listeners[event]?.forEach(h => h(data));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on('login', ({ userId, timestamp }) => {
console.log(`${userId} logged in at ${timestamp}`);
});
emitter.emit('login', { userId: 'u-1', timestamp: new Date() }); // OK
emitter.emit('login', { userId: 'u-1' }); // Error: missing timestampUtility Types from Scratch
Implementing built-in utilities from scratch is the best way to understand type-level programming:
// Exclude<T, U> — remove members of U from T
type MyExclude<T, U> = T extends U ? never : T;
type Nums = MyExclude<string | number | boolean, string>; // number | boolean
// Extract<T, U> — keep only members of T that extend U
type MyExtract<T, U> = T extends U ? T : never;
type Strs = MyExtract<string | number | boolean, string | boolean>; // string | boolean
// NonNullable<T>
type MyNonNullable<T> = T extends null | undefined ? never : T;
// Required<T>
type MyRequired<T> = {
[K in keyof T]-?: T[K]; // -? removes the optional modifier
};
// Mutable<T> — opposite of Readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K]; // -readonly removes the readonly modifier
};Higher-Kinded Types Simulation
TypeScript does not have first-class higher-kinded types (type constructors as arguments), but you can simulate them with an interface-based encoding:
// Register type constructors in a "URI" map
interface URItoKind<A> {
'Array': Array<A>;
'Set': Set<A>;
'Promise': Promise<A>;
}
type URIS = keyof URItoKind<unknown>;
// HKT<F, A> = apply the type constructor F to argument A
type HKT<F extends URIS, A> = URItoKind<A>[F];
// A generic functor interface
interface Functor<F extends URIS> {
map<A, B>(fa: HKT<F, A>, f: (a: A) => B): HKT<F, B>;
}
// Implement for Array
const ArrayFunctor: Functor<'Array'> = {
map: (arr, f) => arr.map(f),
};
// Implement for Set
const SetFunctor: Functor<'Set'> = {
map: (set, f) => new Set([...set].map(f)),
};Common Patterns Reference
Pattern | Syntax | Use case |
|---|---|---|
Conditional | T extends U ? A : B | Branch on type relationships |
Infer | infer R inside extends | Extract sub-types from patterns |
Mapped | { [K in keyof T]: ... } | Transform all properties of a type |
Key remapping | [K in keyof T as NewKey] | Rename or filter properties |
Template literal |
| Construct string literal types |
Recursive | type F<T> = T extends ... ? F<...> : ... | Deep transformations |
Distribution | bare T in conditional | Apply to each union member separately |
No distribution | [T] extends [...] | Treat union as a whole |
Key Takeaways
TypeScript's type system is Turing-complete — types can compute types
Conditional types (T extends U ? A : B) are the if/else of the type level
infer extracts sub-types from patterns, like regex capture groups for types
Mapped types transform every property — add readonly, remove optional, rename keys
Template literal types build string literal types from existing types
Recursive types enable deep transformations like DeepReadonly and DeepPartial
Conditional types distribute over unions when T is a naked type parameter
Use [T] extends [...] to prevent distribution and treat the union as a whole