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.

7 min read

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:

typescripttypescript
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:

typescripttypescript
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):

typescripttypescript
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:

typescripttypescript
type Awaited<T> = T extends PromiseLike<infer U> ? Awaited<U> : T;
 
type Val = Awaited<Promise<Promise<number>>>;
//   ^? number

Awaited 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:

Recursive Awaited type evaluation

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:

typescripttypescript
type DeepFlatten<T> = T extends ReadonlyArray<infer U> ? DeepFlatten<U> : T;
 
type Flat = DeepFlatten<number[][][]>;
//   ^? number

Each 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:

typescripttypescript
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 properties

DeepReadonly 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:

texttext
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

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.
RunePowered by Rune AI

Frequently Asked Questions

What is a recursive type in TypeScript?

A recursive type is a type alias that references itself in its own definition. It is used to describe structures that contain nested versions of themselves, like trees, linked lists, or deeply nested arrays.

Is there a recursion depth limit?

Yes. TypeScript has an internal recursion depth limit. If a recursive type resolves too deeply, TypeScript reports a compile-time error. In practice, this limit is high enough for most real-world use cases.

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.