Recursive Types
A recursive type refers to itself in its own definition, which lets you model arbitrarily nested data such as JSON values, file system trees, linked lists, and comment threads. TypeScript supports recursion in both object/interface types and in generic type aliases, including recursive conditional types that walk a structure at the type level. Recursion is essential for shapes whose depth is not known in advance, but it comes with a compiler depth limit you must respect. This page covers self-referential data types, recursive generics, type-level recursion over tuples, and the practical limits.
Self-referential data types
The most common recursion describes a node that contains more nodes of the same type. Interfaces and type aliases can both reference themselves directly.
interface TreeNode<T> {
value: T;
children: TreeNode<T>[];
}
const root: TreeNode<string> = {
value: "root",
children: [
{ value: "child", children: [] },
],
};
TreeNode<T> mentions itself in its children property, so a tree of any depth is type-checked uniformly. The recursion terminates naturally at runtime whenever children is an empty array.
Modeling JSON
A recursive union is the idiomatic way to type any valid JSON value. It captures the primitive leaves plus the two recursive containers, objects and arrays.
type JsonValue =
| string
| number
| boolean
| null
| JsonValue[]
| { [key: string]: JsonValue };
const data: JsonValue = {
name: "Ada",
active: true,
scores: [10, 20, null],
meta: { nested: { deep: true } },
};
// const bad: JsonValue = { fn: () => 1 }; // ❌ Error: functions aren't JSON
JsonValue allows itself inside arrays and object values, so any depth of nesting is valid, while non-serializable values like functions or undefined are correctly rejected. This single type is far safer than any for handling parsed JSON.
Recursive generic aliases
Generic type aliases can recurse to transform nested structures. A classic example is a deep Readonly that freezes every level of an object.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};
interface Config {
server: { host: string; ports: number[] };
}
type Frozen = DeepReadonly<Config>;
// { readonly server: { readonly host: string; readonly ports: readonly number[] } }
For each property, DeepReadonly checks whether the value is an object; if so it recurses, otherwise it leaves the primitive alone. The readonly modifier is applied at every level, producing a fully immutable view.
Type-level recursion over tuples
Recursive conditional types can iterate tuples one element at a time using rest patterns with infer. This drives type-level algorithms like reversing or counting.
type Reverse<T extends readonly unknown[]> =
T extends [infer Head, ...infer Tail]
? [...Reverse<Tail>, Head]
: [];
type R = Reverse<[1, 2, 3]>; // [3, 2, 1]
type Length<T extends readonly unknown[]> = T["length"];
type L = Length<[1, 2, 3]>; // 3
Reverse peels off Head, recurses on Tail, then appends Head at the end; the empty-tuple base case stops the recursion. Tail-recursive accumulator patterns like this are what TypeScript optimizes best.
| Use case | Recursive shape |
|---|---|
| Tree / hierarchy | { value: T; children: Node<T>[] } |
| JSON value | union including JsonValue[] and { [k]: JsonValue } |
| Linked list | { value: T; next: List<T> | null } |
| Deep transform | mapped type recursing on T[K] extends object |
| Tuple algorithm | T extends [infer H, ...infer R] ? ... : base |
Depth limits and tail recursion
TypeScript caps recursive type instantiation to protect the compiler. Plain recursion errors around 50 levels deep, but since TypeScript 4.5 tail-recursive conditional types — where the recursive call is the entire result — are optimized and support up to roughly 1,000 iterations.
// Tail-recursive: the recursive call IS the result (optimized)
type BuildTuple<N extends number, Acc extends unknown[] = []> =
Acc["length"] extends N ? Acc : BuildTuple<N, [...Acc, unknown]>;
type Five = BuildTuple<5>["length"]; // 5
// Non-tail-recursive deep nesting can hit:
// ❌ Error: Type instantiation is excessively deep and possibly infinite.
BuildTuple accumulates into Acc and returns the recursive call directly, so it qualifies for tail-call optimization and handles large N. Structuring recursion this way is the key to avoiding the “excessively deep” error.
If you hit the depth limit, restructure the type to be tail-recursive with an accumulator, or cap the recursion with an explicit depth counter parameter.
Best Practices
- Use recursive interfaces or type aliases for genuinely nested data like trees and JSON.
- Prefer a recursive
JsonValueunion overanywhen handling parsed JSON. - Always provide a base case (empty tuple,
null, or a non-object branch) to terminate. - Write type-level recursion as tail-recursive with an accumulator to raise the depth ceiling.
- Iterate tuples with
[infer Head, ...infer Tail]patterns rather than indexing manually. - Keep recursion shallow where possible; deep instantiation slows the compiler.
- If you see “excessively deep”, refactor to tail recursion or add a depth-limit parameter.