TypeScriptSeniorTechnical

Как сделать тип рекурсивным в TypeScript? Приведите пример.

Рекурсивный тип ссылается на себя в собственном определении: type JsonValue = string | number | JsonValue[] | { [k: string]: JsonValue }. Используйте interface для структур данных (ленивое вычисление) и type для mapped/conditional рекурсивных утилит.

Рекурсивные типы в TypeScript

TypeScript поддерживает рекурсивные типы: тип может ссылаться на самого себя при определении. Это позволяет описывать вложенные структуры произвольной глубины — деревья, JSON, вложенные меню, AST и т.д.

Базовый пример — JSON Value

type JsonPrimitive = string | number | boolean | null;

type JsonValue =
  | JsonPrimitive
  | JsonValue[]          // массив JSON-значений
  | { [key: string]: JsonValue }; // объект с JSON-значениями

// Использование:
const data: JsonValue = {
  name: 'Alice',
  scores: [1, 2, 3],
  meta: { active: true, tags: ['a', 'b'] },
};

Дерево узлов

interface TreeNode<T> {
  value: T;
  children?: TreeNode<T>[];
}

const tree: TreeNode<string> = {
  value: 'root',
  children: [
    { value: 'child1' },
    {
      value: 'child2',
      children: [
        { value: 'grandchild' },
      ],
    },
  ],
};

function mapTree<T, U>(node: TreeNode<T>, fn: (v: T) => U): TreeNode<U> {
  return {
    value: fn(node.value),
    children: node.children?.map((c) => mapTree(c, fn)),
  };
}

DeepPartial — рекурсивный утилитный тип

type DeepPartial<T> = T extends object
  ? { [K in keyof T]?: DeepPartial<T[K]> }
  : T;

interface Config {
  server: {
    host: string;
    port: number;
    tls: { cert: string; key: string };
  };
  debug: boolean;
}

type PartialConfig = DeepPartial<Config>;

const patch: PartialConfig = {
  server: { port: 443 }, // остальные поля опциональны
};

DeepReadonly

type DeepReadonly<T> = T extends (infer U)[]
  ? ReadonlyArray<DeepReadonly<U>>
  : T extends object
  ? { readonly [K in keyof T]: DeepReadonly<T[K]> }
  : T;

Рекурсивные условные типы (TS 4.1+)

// Путь в объекте через точку: 'a.b.c'
type Paths<T> = T extends object
  ? {
      [K in keyof T & string]:
        | K
        | `${K}.${Paths<T[K]> & string}`;
    }[keyof T & string]
  : never;

interface Form {
  user: { name: string; age: number };
  active: boolean;
}

type FormPaths = Paths<Form>;
// 'user' | 'user.name' | 'user.age' | 'active'

Ограничения компилятора

TypeScript ограничивает глубину рекурсии. При слишком глубоких типах выдаётся: Type instantiation is excessively deep and possibly infinite. Обычный предел — около 50 уровней для дистрибутивных условных типов.

// Обходной путь через интерфейс (lazy evaluation)
// Интерфейсы вычисляются лениво, в отличие от type aliases
interface NestedArray<T> extends Array<T | NestedArray<T>> {}

// Вместо:
type NestedArray<T> = T | NestedArray<T>[]; // может вызвать бесконечный цикл

Подводные камни

  • Type instantiation is excessively deep — компилятор имеет жёсткий лимит рекурсии; при его достижении нужно переписать тип через интерфейс или упростить логику.
  • type alias vs interface для рекурсииtype вычисляется жадно, interface — лениво. Для рекурсивных структур данных предпочтительнее interface; для рекурсивных mapped types — type.
  • DeepPartial с массивами — по умолчанию DeepPartial<T[]> даёт DeepPartial<T>[], что редко нужно; часто массивы нужно обрабатывать отдельно.
  • Циклические зависимости типов — тип не может ссылаться на самого себя через type X = X; нужна хотя бы одна условная ветка или обёртка.
  • Производительность компилятора — глубокие рекурсивные типы резко замедляют tsc и языковой сервер; следите за временем проверки через --diagnostics.
  • Потеря точности в union-распределении — когда T в условном типе является union, TypeScript распределяет рекурсию по каждому члену, что может экспоненциально увеличить количество вычислений.
  • Глубина стека в runtime — рекурсивный тип красив в compile-time, но рекурсивная runtime-функция (mapTree, deepClone) тоже должна обрабатывать чрезмерную вложенность через явный лимит глубины или итеративный подход.
  • infer в рекурсивных типах — при использовании infer внутри рекурсии легко получить непредсказуемые результаты; тестируйте граничные случаи через type Test = Expect<Equal<...>>.

Common mistakes

  • Смешивать «рекурсивные типы» с похожим механизмом без критерия выбора.
  • Игнорировать риск: неверно оценить границы применения темы «рекурсивные типы» и получить хрупкое решение.
  • Показывать только синтаксис и не объяснять поведение в runtime или сборке.

What the interviewer is testing

  • Объясняет описание деревьев, JSON и вложенных структур.
  • Показывает на примере, как работает: рекурсивный тип ссылается на себя через свойства или union; важно добавить базовый случай, иначе тип становится тяжелым для компилятора и непонятным для API.
  • Называет production-нюанс и граничный случай для темы «рекурсивные типы».

Sources

Related topics