Type-Level Programming & Recursive Types
📖 Concept
TypeScript's type system is Turing complete — you can perform arbitrary computation at the type level. While you rarely need this for application code, understanding type-level programming lets you build powerful library types and understand complex type definitions.
Recursive types reference themselves in their definition — essential for tree structures, linked lists, JSON types, and deep utility types.
Type-level programming techniques:
- Recursive conditional types
- Tuple manipulation (Head, Tail, Concat, Reverse)
- String manipulation at type level
- Type-level arithmetic (limited)
- Type-level pattern matching
Real-world uses:
DeepPartial<T>— Recursively make all properties optionalPathsOf<T>— Generate all valid dot-notation pathsDeepReadonly<T>— Recursively make immutable- JSON type definition — Self-referencing type
- Zod/io-ts schema inference
Important caveat: TypeScript has a recursion depth limit (~50 levels). Deeply recursive types can hit this limit and cause "Type instantiation is excessively deep" errors. Design types to have bounded recursion.
🏠 Real-world analogy: Type-level programming is like a Russian nesting doll inspector. You open each doll (recurse), check what's inside, and classify or transform it. Each level follows the same rules until you reach the innermost doll (base case).
💻 Code Example
1// Recursive type: JSON2type JSON =3 | string4 | number5 | boolean6 | null7 | JSON[]8 | { [key: string]: JSON };910// DeepPartial — recursively make all optional11type DeepPartial<T> = {12 [K in keyof T]?: T[K] extends object13 ? DeepPartial<T[K]>14 : T[K];15};1617interface Config {18 db: { host: string; port: number; ssl: { enabled: boolean; cert: string } };19 cache: { ttl: number };20}21type PartialConfig = DeepPartial<Config>;22// db?.host? is optional, db?.ssl?.enabled? is optional, etc.2324// Tuple manipulation25type Head<T extends any[]> = T extends [infer H, ...any[]] ? H : never;26type Tail<T extends any[]> = T extends [any, ...infer R] ? R : never;27type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;28type Length<T extends any[]> = T['length'];2930type H = Head<[1, 2, 3]>; // 131type T2 = Tail<[1, 2, 3]>; // [2, 3]32type L = Last<[1, 2, 3]>; // 33334// Reverse a tuple35type Reverse<T extends any[]> = T extends [infer First, ...infer Rest]36 ? [...Reverse<Rest>, First]37 : [];38type Rev = Reverse<[1, 2, 3, 4]>; // [4, 3, 2, 1]3940// Flatten nested arrays at type level41type Flatten<T extends any[]> = T extends [infer First, ...infer Rest]42 ? First extends any[]43 ? [...Flatten<First>, ...Flatten<Rest>]44 : [First, ...Flatten<Rest>]45 : [];46type Flat = Flatten<[1, [2, 3], [4, [5]]]>; // [1, 2, 3, 4, 5]4748// String manipulation at type level49type TrimLeft<S extends string> = S extends ` ${infer R}` ? TrimLeft<R> : S;50type TrimRight<S extends string> = S extends `${infer R} ` ? TrimRight<R> : S;51type Trim<S extends string> = TrimLeft<TrimRight<S>>;52type Trimmed = Trim<" hello ">; // "hello"5354// Split a string type55type Split<S extends string, D extends string> =56 S extends `${infer Head}${D}${infer Tail}`57 ? [Head, ...Split<Tail, D>]58 : [S];59type Parts = Split<"a.b.c", ".">; // ["a", "b", "c"]6061// Deep property path type62type Paths<T, D extends string = "."> = T extends object63 ? {64 [K in keyof T & string]: T[K] extends object65 ? K | `${K}${D}${Paths<T[K], D>}`66 : K;67 }[keyof T & string]68 : never;6970interface User {71 name: string;72 address: {73 street: string;74 city: string;75 geo: { lat: number; lng: number };76 };77}78type UserPaths = Paths<User>;79// "name" | "address" | "address.street" | "address.city" |80// "address.geo" | "address.geo.lat" | "address.geo.lng"8182// Type-safe deep get83type DeepGet<T, P extends string> =84 P extends `${infer K}.${infer Rest}`85 ? K extends keyof T86 ? DeepGet<T[K], Rest>87 : never88 : P extends keyof T89 ? T[P]90 : never;9192type CityType = DeepGet<User, "address.city">; // string93type LatType = DeepGet<User, "address.geo.lat">; // number
🏋️ Practice Exercise
Mini Exercise:
- Implement
DeepReadonly<T>that recursively makes all properties readonly - Create a
Concat<A, B>type that concatenates two tuple types - Build a
Split<S, Delimiter>type for string splitting at the type level - Write
Paths<T>that generates all dot-notation paths for an object type - Implement a type-safe
get(obj, "a.b.c")function using recursive types
⚠️ Common Mistakes
Hitting the recursion depth limit (~50 levels) — design types with bounded recursion or use
@ts-expect-errorpragmaticallyRecursive types that expand exponentially — each level doubles the work; this crashes the TypeScript compiler
Forgetting the base case in recursive types — always handle the terminal condition (
T extends []for arrays)Overusing type-level programming in application code — it's powerful but makes types hard to read and debug; save it for libraries
Not using
& stringwhen iteratingkeyof Twith template literals —keyof Tincludessymbol, which can't be used in template literals
💼 Interview Questions
🎤 Mock Interview
Practice a live interview for Type-Level Programming & Recursive Types