Skip to content
TypeScript ts advanced-types 4 min read

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 caseRecursive shape
Tree / hierarchy{ value: T; children: Node<T>[] }
JSON valueunion including JsonValue[] and { [k]: JsonValue }
Linked list{ value: T; next: List<T> | null }
Deep transformmapped type recursing on T[K] extends object
Tuple algorithmT 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 JsonValue union over any when 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.
Last updated June 29, 2026
Was this helpful?