Recursive Types

Recursive Types (рекурсивные типы) — типы TypeScript, которые ссылаются на себя в своём определении, позволяя описывать древовидные и вложенные структуры данных произвольной глубины.

Зачем нужно

Многие структуры данных рекурсивны по природе: деревья, JSON, файловые системы, вложенные комментарии, AST. TypeScript поддерживает рекурсию в type alias (с TS 3.7+) и в интерфейсах, что позволяет точно типизировать такие структуры.

Где используется

  • JSON-тип (string | number | boolean | null | JsonArray | JsonObject)
  • Дерево категорий, меню с вложенностью
  • Узлы AST и компиляторов
  • Рекурсивные utility types: DeepPartial, DeepReadonly
  • Файловая система: директории содержат файлы и директории

Основной контент

Рекурсивный интерфейс

interface TreeNode<T> {
  value: T;
  children: TreeNode<T>;
}

const tree: TreeNode<string> = {
  value: "root",
  children: [
    {
      value: "branch-1",
      children: [
        { value: "leaf-1", children:  },
        { value: "leaf-2", children:  },
      ],
    },
    { value: "branch-2", children:  },
  ],
};

function traverse<T>(node: TreeNode<T>, fn: (value: T) => void): void {
  fn(node.value);
  node.children.forEach((child) => traverse(child, fn));
}

JSON-тип

type JsonPrimitive = string | number | boolean | null;
type JsonObject    = { [key: string]: JsonValue };
type JsonArray     = JsonValue;
type JsonValue     = JsonPrimitive | JsonObject | JsonArray;

const data: JsonValue = {
  name: "Alice",
  age: 30,
  tags: ["admin", "user"],
  address: {
    city: "Moscow",
    coords: [55.75, 37.62],
  },
};

Рекурсивные utility types

// DeepPartial — все поля на всех уровнях опциональные
type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

// DeepReadonly — все поля на всех уровнях readonly
type DeepReadonly<T> = T extends (infer U)
  ? ReadonlyArray<DeepReadonly<U>>
  : T extends object
    ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
    : T;

interface Config {
  db: { host: string; port: number };
  cache: { ttl: number; maxSize: number };
}

type PartialConfig = DeepPartial<Config>;
// { db?: { host?: string; port?: number }; cache?: { ttl?: number; maxSize?: number } }

type ImmutableConfig = DeepReadonly<Config>;
// Все поля readonly на всех уровнях

Категории с вложенностью

interface Category {
  id: string;
  name: string;
  subcategories: Category;
}

function findById(categories: Category, id: string): Category | undefined {
  for (const cat of categories) {
    if (cat.id === id) return cat;
    const found = findById(cat.subcategories, id);
    if (found) return found;
  }
  return undefined;
}

Ограничение глубины рекурсии (TS 4.1+)

// Тип для ограниченной глубины (Depth = 0 → never)
type Prev = [never, 0, 1, 2, 3, 4, 5];

type DeepPartialWithDepth<T, D extends number = 5> = [D] extends [never]
  ? never
  : T extends object
    ? { [K in keyof T]?: DeepPartialWithDepth<T[K], Prev[D]> }
    : T;

Частые ошибки

  • Бесконечная рекурсия типа — TypeScript ограничивает глубину рекурсии; слишком глубокие типы вызовут ошибку Type instantiation is excessively deep.
  • type alias без объектного wrappertype A = A вызовет ошибку; рекурсия должна быть через объект или условный тип.
  • Не обрабатывать базовый случай — в функциях, обходящих рекурсивный тип, обязательно нужна проверка на пустой массив/null-терминал.
  • Конфузить структурную и рекурсивную совместимость — TypeScript принимает более узкие реализации, но не уберегает от зацикливания в рантайме.

Связанные темы

Ресурсы