On this page · sections
Type-Level Programming
Type-level programming treats TypeScript’s type system as a language of its own — you compute types from other types using recursion, conditionals, and tuple manipulation. This is how libraries like ts-toolbelt, type-fest, and internal design-system utilities are built.
| Technique | What it computes | Typical use |
|---|---|---|
| Recursive types | Nested structures | DeepPartial, JSON paths |
| Distributive conditionals | Per-union-member branches | Filter unions, unwrap arrays |
| Tuple manipulation | Head, tail, length, concat | Function arity, route params |
| Type-level arithmetic | Add, subtract on number literals | Tuple length proofs (advanced) |
Recursive types
Definition
A type references itself to model arbitrarily nested structures:
type Json =
| string
| number
| boolean
| null
| Json[]
| { [key: string]: Json };
type DeepPartial<T> = T extends (...args: never[]) => unknown
? T
: T extends object
? { [K in keyof T]?: DeepPartial<T[K]> }
: T;Each level unwraps T and recurses into property values. Functions are typically left untouched so callbacks remain callable.
Real-world example — path typing
type Paths<T, Prefix extends string = ""> = T extends object
? {
[K in keyof T & string]: T[K] extends object
? `${Prefix}${K}` | Paths<T[K], `${Prefix}${K}.`>
: `${Prefix}${K}`;
}[keyof T & string]
: never;
type Config = {
api: { url: string; timeout: number };
flags: { beta: boolean };
};
type ConfigPath = Paths<Config>;
// "api" | "api.url" | "api.timeout" | "flags" | "flags.beta"Interview: Recursive types need a base case (primitives, functions) or TypeScript hits “type instantiation is excessively deep.”
Distributive conditional types
Definition
When the checked type is a naked type parameter, T extends U ? X : Y distributes over unions:
type ToArray<T> = T extends unknown ? T[] : never;
type R = ToArray<string | number>;
// string[] | number[]This is not runtime iteration — TypeScript evaluates each union member separately and unions the results.
Disabling distribution
Wrap the type parameter in a tuple to prevent distribution:
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type R2 = ToArrayNonDist<string | number>;
// (string | number)[]Filtering unions
type ExtractStrings<T> = T extends string ? T : never;
type OnlyStrings = ExtractStrings<"a" | 1 | "b" | true>;
// "a" | "b"Built-in Extract and Exclude use this same mechanism.
infer inside distributive conditionals
type UnwrapArray<T> = T extends (infer E)[] ? E : T;
type A = UnwrapArray<string[]>; // string
type B = UnwrapArray<number>; // numberInterview: Distribution is why
Exclude<T, U>removes union members one at a time. Interviewers often ask you to predictstring | number extends string ? A : B.
Tuple manipulation
Definition
Tuples are fixed-length arrays with known element types — ideal for type-level list operations.
type Head<T extends readonly unknown[]> = T extends readonly [infer H, ...unknown[]]
? H
: never;
type Tail<T extends readonly unknown[]> = T extends readonly [unknown, ...infer Rest]
? Rest
: [];
type Length<T extends readonly unknown[]> = T["length"];
type First = Head<[1, 2, 3]>; // 1
type Rest = Tail<[1, 2, 3]>; // [2, 3]
type Len = Length<[1, 2, 3]>; // 3Concat and reverse
type Concat<
A extends readonly unknown[],
B extends readonly unknown[],
> = [...A, ...B];
type Reverse<T extends readonly unknown[], Acc extends readonly unknown[] = []> =
T extends readonly [infer H, ...infer Rest]
? Reverse<Rest, [H, ...Acc]>
: Acc;
type R = Reverse<[1, 2, 3]>; // [3, 2, 1]Function arity from tuples
type Curry<Args extends readonly unknown[], R> = Args extends readonly [
infer H,
...infer Rest,
]
? (arg: H) => Curry<Rest, R>
: R;
type Add = (a: number, b: number, c: number) => number;
type CurriedAdd = Curry<[number, number, number], number>;
// (arg: number) => (arg: number) => (arg: number) => numberType-level computation patterns
Flatten nested unions
type Flatten<T> = T extends readonly (infer Inner)[]
? Flatten<Inner>
: T;
type Nested = [[[1, 2], 3], 4];
type Flat = Flatten<Nested>; // 1 | 2 | 3 | 4Replace keys in a union
type Replace<
T extends string,
From extends string,
To extends string,
> = T extends `${infer Pre}${From}${infer Post}`
? `${Pre}${To}${Post}`
: T;
type Renamed = Replace<"getUserById", "User", "Account">;
// "getAccountById"Last element of a tuple
type Last<T extends readonly unknown[]> = T extends readonly [...unknown[], infer L]
? L
: never;
type L = Last<[1, 2, 3]>; // 3When to use type-level programming
In application code
Reach for built-in utilities first (Partial, Pick, ReturnType). Write custom recursive types when:
- A design system needs polymorphic prop inference
- An API client maps endpoint strings to request/response types
- A form library types field paths from a schema
In library code
Type-level programming shines in published libraries where consumers depend on precise inference:
type EventMap = {
click: { x: number; y: number };
keydown: { key: string };
};
type EventName = keyof EventMap;
function on<E extends EventName>(
event: E,
handler: (payload: EventMap[E]) => void,
): void {
// implementation
}
on("click", (p) => console.log(p.x)); // p is { x: number; y: number }Debugging type-level code
Techniques
- Hover in the IDE — assign intermediate types to
type Debug = ...aliases. - Simplify — remove recursion first; confirm the base case works.
- Check distribution — wrap in
[T]if union behavior is unexpected. - Limit depth — add a depth counter generic parameter for very deep trees.
type Depth = [never, 0, 1, 2, 3, 4, 5];
type SafeDeepPartial<T, D extends number = 5> = [D] extends [never]
? T
: T extends object
? { [K in keyof T]?: SafeDeepPartial<T[K], Depth[D]> }
: T;Senior Interview Summary
What to say
Type-level programming uses recursive types for nested structures, distributive conditionals to transform unions member-by-member, and tuple manipulation for arity and list operations. It’s powerful in libraries but should stay behind clean public APIs in application code.
Mental model
Recursive type → base case + self-reference
Distributive → naked T in T extends U ? A : B
Tuple ops → infer head/tail, spread [...A, ...B]
Too deep error → missing base case or unbounded recursionWhen interviewers expect depth
Predict union distribution → wrap in tuple to disable
Implement Flatten / DeepPartial → recursive conditional + mapped
Curry / pipe tuples → infer + spread
Everyday feature code → built-ins first; custom only when justifiedPractical rule
Libraries & shared types → invest in type-level utilities
Feature components → explicit props > clever meta-programming
Debug with type aliases → never guess what a 40-line conditional resolves to