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-нюанс и граничный случай для темы «рекурсивные типы».