Type-Level Programming & Recursive Types

0/5 in this phase0/21 across the roadmap

📖 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 optional
  • PathsOf<T> — Generate all valid dot-notation paths
  • DeepReadonly<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

codeTap to expand ⛶
1// Recursive type: JSON
2type JSON =
3 | string
4 | number
5 | boolean
6 | null
7 | JSON[]
8 | { [key: string]: JSON };
9
10// DeepPartial — recursively make all optional
11type DeepPartial<T> = {
12 [K in keyof T]?: T[K] extends object
13 ? DeepPartial<T[K]>
14 : T[K];
15};
16
17interface 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.
23
24// Tuple manipulation
25type 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'];
29
30type H = Head<[1, 2, 3]>; // 1
31type T2 = Tail<[1, 2, 3]>; // [2, 3]
32type L = Last<[1, 2, 3]>; // 3
33
34// Reverse a tuple
35type 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]
39
40// Flatten nested arrays at type level
41type 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]
47
48// String manipulation at type level
49type 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"
53
54// Split a string type
55type 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"]
60
61// Deep property path type
62type Paths<T, D extends string = "."> = T extends object
63 ? {
64 [K in keyof T & string]: T[K] extends object
65 ? K | `${K}${D}${Paths<T[K], D>}`
66 : K;
67 }[keyof T & string]
68 : never;
69
70interface 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"
81
82// Type-safe deep get
83type DeepGet<T, P extends string> =
84 P extends `${infer K}.${infer Rest}`
85 ? K extends keyof T
86 ? DeepGet<T[K], Rest>
87 : never
88 : P extends keyof T
89 ? T[P]
90 : never;
91
92type CityType = DeepGet<User, "address.city">; // string
93type LatType = DeepGet<User, "address.geo.lat">; // number

🏋️ Practice Exercise

Mini Exercise:

  1. Implement DeepReadonly<T> that recursively makes all properties readonly
  2. Create a Concat<A, B> type that concatenates two tuple types
  3. Build a Split<S, Delimiter> type for string splitting at the type level
  4. Write Paths<T> that generates all dot-notation paths for an object type
  5. 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-error pragmatically

  • Recursive 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 & string when iterating keyof T with template literals — keyof T includes symbol, which can't be used in template literals

💼 Interview Questions

🎤 Mock Interview

Practice a live interview for Type-Level Programming & Recursive Types