Topics
v5.0.0
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.

TechniqueWhat it computesTypical use
Recursive typesNested structuresDeepPartial, JSON paths
Distributive conditionalsPer-union-member branchesFilter unions, unwrap arrays
Tuple manipulationHead, tail, length, concatFunction arity, route params
Type-level arithmeticAdd, subtract on number literalsTuple 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>; // number

Interview: Distribution is why Exclude<T, U> removes union members one at a time. Interviewers often ask you to predict string | 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]>; // 3

Concat 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) => number

Type-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 | 4

Replace 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]>; // 3

When 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

  1. Hover in the IDE — assign intermediate types to type Debug = ... aliases.
  2. Simplify — remove recursion first; confirm the base case works.
  3. Check distribution — wrap in [T] if union behavior is unexpected.
  4. 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 recursion

When 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 justified

Practical 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

Last updated on