Recursive Types in TypeScript
Recursive types reference themselves to describe nested or self-similar structures like trees, linked lists, JSON, and deeply nested arrays or promises.
TypeScript recursive types are types that refer to themselves in their own definition. They describe structures where a piece contains smaller pieces of the same shape: a tree node that holds child nodes, a JSON value that can contain nested objects, or a linked list where each node points to the next.
The simplest recursive type is a tree node:
type TreeNode = {
value: string;
children: TreeNode[];
};
const tree: TreeNode = {
value: "root",
children: [
{ value: "child-1", children: [{ value: "grandchild-1", children: [] }] },
{ value: "child-2", children: [] },
],
};TreeNode references itself through the children property. Each node can hold more nodes of the same shape. TypeScript understands this and allows arbitrarily deep nesting.
Recursive types for JSON
The JSON data model is naturally recursive: a JSON value can be a string, number, boolean, null, an array of JSON values, or an object whose values are JSON values:
type JSONValue =
| string
| number
| boolean
| null
| JSONValue[]
| { [key: string]: JSONValue };
const data: JSONValue = {
user: "alice",
scores: [95, 87, 92],
details: {
active: true,
lastLogin: null,
history: [{ action: "login", timestamp: "2026-01-01" }],
},
};The JSONValue type covers any valid JSON structure. Because objects and arrays both reference JSONValue, the type adapts to any depth of nesting.
Linked lists and other self-referential structures
A linked list node contains a value and a reference to the next node (or null):
type ListNode<T> = {
value: T;
next: ListNode<T> | null;
};
const list: ListNode<number> = {
value: 1,
next: {
value: 2,
next: {
value: 3,
next: null,
},
},
};The same pattern describes file system nodes, DOM-like element trees, and any structure where a thing contains more things of the same kind.
Recursive conditional types
TypeScript 4.1 introduced recursive conditional types -- conditional types that call themselves in their branches. This is useful for transforming types that are wrapped in multiple layers.
The most common example is Awaited, which unwraps nested promises:
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
type Val = Awaited<Promise<Promise<number>>>;
// ^? numberAwaited checks if T is a promise. If so, it captures the resolved type as U and calls itself on U. This peels off one layer at a time until the value is no longer a promise.
Here is the flow for a doubly-wrapped promise:
Each recursive call strips one Promise wrapper. When the base case (not a promise) is reached, the type returns unchanged.
Deep flattening nested arrays
A recursive conditional type can flatten arrays of any depth:
type DeepFlatten<T> = T extends ReadonlyArray<infer U> ? DeepFlatten<U> : T;
type Flat = DeepFlatten<number[][][]>;
// ^? numberEach iteration peels off one array layer and captures the element type. The recursion continues until the element type is not an array.
Recursive mapped types
You can combine mapped types with recursion to deeply apply transformations:
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K];
};
interface User {
name: string;
address: {
street: string;
city: string;
};
}
type ReadonlyUser = DeepReadonly<User>;
// All properties are readonly, including nested address propertiesDeepReadonly makes every property readonly, then recurses into non-function object properties. Functions and primitives stop the recursion. For a deeper exploration, see Build a DeepReadonly Type in TypeScript.
Recursion depth limits
TypeScript has an internal recursion depth limit. If a recursive type resolves too deeply, the compiler reports an error:
Type instantiation is excessively deep and possibly infinite.This almost never happens with real data structures. A JSON response, a DOM tree, or a linked list rarely exceeds a few hundred levels of nesting. The limit is typically high enough for practical use.
If you do hit the limit, consider these strategies:
- Flatten the structure. A deep linked list can become a flat array with indexes instead of references.
- Use an iterative type instead of a recursive one when possible.
- Break the recursion into smaller steps with intermediate type aliases.
When not to use recursive types
Recursive types are the right tool for genuinely recursive data. They are not the right tool for:
- Very deep structures that should be flat. If your data has thousands of nesting levels, the type is reflecting a design problem in the data, not a limitation in TypeScript.
- Performance-sensitive generic types. Recursive conditional types can increase type-checking time. For library types that will be used widely, benchmark the impact.
- Situations where a union or mapped type is simpler. Not every nested structure needs recursion. A two-level nested type can just be two type aliases. For guidance on when advanced types add more complexity than they solve, see When Advanced Types Become Too Complex.
Common mistakes
Forgetting the base case. Every recursive type needs a branch that stops the recursion. Without one, TypeScript may report an excessively deep instantiation error.
Confusing recursive types with runtime recursion. A recursive type describes the shape of data at compile time. It does not produce recursive runtime code or affect how the data is processed.
Using recursion when a loop pattern exists. If the number of layers is fixed and small (say, exactly three levels), write three separate types instead of a recursive one. The intent is clearer.
Rune AI
Key Insights
- A recursive type references itself in its own definition.
- Use recursive types for tree nodes, JSON, nested arrays, and deeply wrapped promises.
- Recursive conditional types peel layers in the true branch and stop at the false branch.
- Always provide a base case that stops the recursion.
- TypeScript has a recursion depth limit; respect it and prefer shallower structures when possible.
Frequently Asked Questions
What is a recursive type in TypeScript?
Is there a recursion depth limit?
Conclusion
Recursive types let you describe self-referential structures like trees, JSON, linked lists, and deeply nested wrappers. Recursive conditional types extend this to type-level transformations that peel off layers one at a time. Keep recursion depths reasonable, add base cases, and prefer iterative patterns for extremely deep structures.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.