Recursive Types
A type that refers to itself
Section titled “A type that refers to itself”Recursion at the type level looks exactly like recursion in code: a type alias that mentions itself. The classic example is a JSON value — data nested to any depth:
type Json = | string | number | boolean | null | Json[] | { [key: string]: Json };
const ok: Json = { name: "Ada", tags: ["x", "y"], meta: { n: 1 } }; // ✅const bad: Json = { fn: () => 1 }; // ❌ functions are not JsonJson appears in its own definition twice — inside the array and inside the object — which is what lets it describe arbitrarily deep data. Recursive aliases like this are common and completely idiomatic.
Recursion over tuples with infer
Section titled “Recursion over tuples with infer”The more powerful pattern is recursing over a tuple type, peeling one element at a time with infer — the type-level equivalent of processing a list head/tail.
// Reverse a tuple typetype Reverse<T extends unknown[]> = T extends [infer Head, ...infer Rest] ? [...Reverse<Rest>, Head] : [];
type R = Reverse<[1, 2, 3]>;// ^? [3, 2, 1]
// Length of a tupletype Length<T extends unknown[]> = T["length"];type L = Length<[1, 2, 3]>; // 3[infer Head, ...infer Rest] splits a tuple into its first element and the rest; the type calls itself on Rest until it hits the empty tuple base case. This is how typed path builders, deep Get<Obj, "a.b.c"> lookups, and tuple math are written.
flowchart LR a["[1, 2, 3]"] --> b["head 1, rest [2, 3]"] b --> c["head 2, rest [3]"] c --> d["head 3, rest []"] d --> e["base case: stop"]
The instantiation-depth limit
Section titled “The instantiation-depth limit”Type-level recursion is not unbounded. The compiler caps recursive instantiation to protect itself from infinite loops, and you will hit the ceiling on large inputs:
// Error on deep recursion:// "Type instantiation is excessively deep and possibly infinite."The historical limit was around 50 levels of naive recursion, later relaxed for tail-recursive conditional types (where the recursive call is the entire result), which can go much deeper — roughly a few thousand. Writing a recursion tail-recursively (accumulate into an extra type parameter, return it at the base case) is the standard trick to push the limit:
// Tail-recursive: accumulate results, so the compiler can optimize ittype BuildTuple<N extends number, Acc extends unknown[] = []> = Acc["length"] extends N ? Acc : BuildTuple<N, [...Acc, unknown]>;Knowing when to stop
Section titled “Knowing when to stop”Recursive types are the sharpest tool in the type-level box, and the easiest to misuse. Before writing one, ask:
- Will a teammate be able to read it? A clever type that no one dares touch is a liability, not an asset.
- Is the error message tolerable? Deep recursive types produce dreadful errors when they fail — sometimes worse than the bug they prevent.
- Would a simpler type or a runtime check do? Often the honest answer is a plain type plus validation at the boundary (the Practical Mastery module).
The best type-level engineers write less of it than they could, reserving recursion for the few cases — deep key paths, precise builder APIs — where it genuinely earns its keep.