В ранних версиях TypeScript массивы и кортежи имели фундаментальное разделение: массивы T[] обладали произвольной длиной с единым гомогенным типом элементов, а кортежи [A, B, C] представляли жестко фиксированные гетерогенные структуры. Любая попытка объединить, обрезать или развернуть кортежи требовала громоздких рукописных перегрузок функций (до 10–15 перегрузок на каждый шаг композиции).
Революция произошла с внедрением Variadic Tuple Types (Вариативных кортежей) в TypeScript 4.0. Оператор спреда (...T) получил первоклассную поддержку внутри позиций кортежей, позволяя моделировать абстрактные цепочки аргументов произвольной размерности: [First, ...Middle[], Last].
Вторым критическим рубежом стало появление Tail-Call Optimization on Types (TCO на уровне типов) в TypeScript 4.5. До этого момента компилятор имел жесткий лимит глубины стека инстанциации рекурсивных типов (~50 шагов), после чего аварийно прерывал сборку с фатальной ошибкой TS2589: "Type instantiation is excessively deep and possibly infinite".
Благодаря механизму TCO рекурсивные условные типы, спроектированные по схеме хвостовой рекурсии с параметром-аккумулятором, оптимизируются компилятором в плоский итерационный цикл. Это увеличило допустимую глубину рекурсии до 1000 вызовов, открыв путь к полноценной реализации классических алгоритмов функционального программирования (Reverse, Filter, Map, Curry) непосредственно в системе статических типов.
Различие между обычной (стековой) и оптимизированной хвостовой рекурсией в компиляторе TypeScript заключается в том, где происходит формирование итогового типа:
+-------------------------------------------------------------------------+
| ОБЫЧНАЯ РЕКУРСИЯ (Лимит: ~50 шагов) |
| Reverse<[1, 2, 3]> => [...Reverse<[2, 3]>, 1] |
+-------------------------------------------------------------------------+
| Стек компилятора вынужден хранить неразрешенные операции спреда: |
| [... [... [...Reverse<[]>, 3], 2], 1] |
| => Переполнение стека tsc при N > 50 (Ошибка TS2589)! |
+-------------------------------------------------------------------------+
| |
| ХВОСТОВАЯ РЕКУРСИЯ (TCO) (Лимит: 1000 шагов!) |
| ReverseTCO<T, Acc = []> |
+-------------------------------------------------------------------------+
| Компилятор сразу передает результат в аккумулятор на каждом шаге: |
| 1. Step 1: T = [2, 3], Acc = [1] |
| 2. Step 2: T = [3], Acc = [2, 1] |
| 3. Step 3: T = [], Acc = [3, 2, 1] |
| => Терминальный выход: возвращается готовый Acc без ожидания стека! |
+-------------------------------------------------------------------------+В хвостовой рекурсии условный тип возвращает непосредственный вызов себя же без оборачивания его в дополнительные типы (кортежи, объединения или пересечения). Это позволяет компилятору повторно использовать текущий фрейм вычисления.
Создается начальный аккумулятор (по умолчанию пустой кортеж Acc = []).
Через [infer Head, ...infer Tail] извлекается первый элемент кортежа.
Извлеченный элемент помещается в аккумулятор [Head, ...Acc].
Компилятор перезаписывает текущий контекст новыми значениями Tail и Acc без аллокации стека.
Когда входной кортеж пуст, рекурсия завершается, возвращая накопленный аккумулятор Acc.
Почему рекурсивный тип с аккумулятором (Reverse<Tail, [Head, ...Acc]>) поддерживает до 1000 шагов, а обычный ([...Reverse<Tail>, Head]) падает на ~50 шагах?
Компилятор валидирует TrueBranch: тип обязан возвращать чистый вызов дженерика без обрамляющих операций спреда.
Вместо создания нового фрейма в стеке компилятор перезаписывает локальные регистры параметров T и Acc.
Счетчик шагов инкрементируется. Если лимит в 1000 итераций не превышен, итерация продолжается.
При достижении базового условия выхода (Base Case) готовый аккумулятор Acc мгновенно эмитится клиенту.
Построим типобезопасный инструментарий для работы с кортежами:
// 1. Извлечение первого элемента (Head)
export type Head<TTuple extends readonly unknown[]> = TTuple extends readonly [
infer THead,
...unknown[]
]
? THead
: never;
// 2. Извлечение хвоста кортежа без первого элемента (Tail)
export type Tail<TTuple extends readonly unknown[]> = TTuple extends readonly [
unknown,
...infer TTail
]
? TTail
: [];
// 3. Добавление элемента в конец кортежа (Push)
export type Push<TTuple extends readonly unknown[], TElement> = [...TTuple, TElement];
// 4. Добавление элемента в начало кортежа (Unshift)
export type Unshift<TTuple extends readonly unknown[], TElement> = [TElement, ...TTuple];
// 5. Извлечение последнего элемента (Last)
export type Last<TTuple extends readonly unknown[]> = TTuple extends readonly [
...unknown[],
infer TLast
]
? TLast
: never;
// 6. Удаление последнего элемента (Pop)
export type Pop<TTuple extends readonly unknown[]> = TTuple extends readonly [
...infer TRest,
unknown
]
? TRest
: [];
// Тестовые проверки
type TestTuple = [string, number, boolean];
type T1 = Head<TestTuple>; // string
type T2 = Tail<TestTuple>; // [number, boolean]
type T3 = Push<TestTuple, Date>; // [string, number, boolean, Date]
type T4 = Last<TestTuple>; // boolean
type T5 = Pop<TestTuple>; // [string, number]Продемонстрируем силу оптимизации хвостовой рекурсии:
// ❌ НЕ-TCO РЕАЛИЗАЦИЯ: компилятор падает на кортеже длиннее 45 элементов!
export type ReverseNaive<T extends readonly unknown[]> = T extends readonly [
infer Head,
...infer Tail
]
? [...ReverseNaive<Tail>, Head] // Наличие спреда [...] снаружи ломает TCO!
: [];
// ✅ TCO РЕАЛИЗАЦИЯ: хвостовой вызов с аккумулятором работает до 1000 элементов!
export type Reverse<
TTuple extends readonly unknown[],
TAccumulator extends readonly unknown[] = []
> = TTuple extends readonly [infer THead, ...infer TTail]
? Reverse<TTail, [THead, ...TAccumulator]> // Чистый хвостовой вызов!
: TAccumulator;
// Проверка работы
type Original = ["a", "b", "c", "d"];
type Inverted = Reverse<Original>; // ["d", "c", "b", "a"]Curry<F>)#Одной из сложнейших вершин type-level инженерии является типизация каррированной функции, поддерживающей частичное применение любого количества аргументов:
// Рекурсивный каррификатор кортежа аргументов
export type Curry<TFunction> = TFunction extends (
...args: infer TArgs
) => infer TReturn
? TArgs extends [infer TFirst, ...infer TRest]
? TRest["length"] extends 0
? (arg: TFirst) => TReturn
: (arg: TFirst) => Curry<(...args: TRest) => TReturn>
: () => TReturn
: never;
// Демонстрация работы каррирования
function calculateOrderTotal(
basePrice: number,
taxRate: number,
discountCode: string,
isVip: boolean
): number {
const discount = isVip ? 0.2 : 0.05;
return basePrice * (1 + taxRate) * (1 - discount);
}
// Заглушка функции curry в рантайме
declare function curry<F extends (...args: any[]) => any>(fn: F): Curry<F>;
const curriedOrder = curry(calculateOrderTotal);
// IDE обеспечивает безупречный последовательный автокомплит каждого шага!
const step1 = curriedOrder(1000); // (taxRate: number) => ...
const step2 = step1(0.2); // (discountCode: string) => ...
const step3 = step2("SUMMER2026"); // (isVip: boolean) => number
const finalPrice = step3(true); // numberDeepReadonly<T>#Рекурсивный обход древовидных объектов и кортежей для предотвращения мутаций:
export type DeepReadonly<T> = T extends Function
? T
: T extends readonly (infer ElementType)[]
? ReadonlyArray<DeepReadonly<ElementType>>
: T extends object
? { readonly [K in keyof T]: DeepReadonly<T[K]> }
: T;
interface ComplexMutableGraph {
rootId: string;
nodes: Array<{
id: number;
metadata: {
tags: string[];
owner: { name: string };
};
}>;
}
export type FrozenGraph = DeepReadonly<ComplexMutableGraph>;
declare const graph: FrozenGraph;
// ❌ Ошибки компиляции на всех уровнях вложенности:
// graph.rootId = "new-id"; // TS2540: Cannot assign to 'rootId'
// graph.nodes.push({ ... }); // TS2339: Property 'push' does not exist on 'ReadonlyArray'
// graph.nodes[0].metadata.tags[0] = "mutated"; // TS2542: Index signature in type 'readonly string[]' only permits readingpipe#Разработаем типобезопасный конвейер композиции функций произвольной длины с автоматической сквозной проверкой совместимости типов звеньев:
export interface UnaryFunction<TIn, TOut> {
(input: TIn): TOut;
}
// Проверка цепочки функций на совместимость контрактов
export type PipelineChain<TInput, TFunctions extends readonly unknown[]> =
TFunctions extends readonly [
UnaryFunction<infer TCurrentIn, infer TCurrentOut>,
...infer TRest
]
? [TInput] extends [TCurrentIn]
? [UnaryFunction<TCurrentIn, TCurrentOut>, ...PipelineChain<TCurrentOut, TRest>]
: never
: [];
// Тип возвращаемого значения последнего звена
export type PipelineOutput<TInput, TFunctions extends readonly unknown[]> =
TFunctions extends readonly [
...unknown[],
UnaryFunction<any, infer TLastOut>
]
? TLastOut
: TInput;
export function createPipeline<TInitial>() {
return function execute<TFunctions extends readonly UnaryFunction<any, any>[]>(
initialValue: TInitial,
...fns: TFunctions & PipelineChain<TInitial, TFunctions>
): PipelineOutput<TInitial, TFunctions> {
return fns.reduce((acc, fn) => fn(acc), initialValue) as PipelineOutput<
TInitial,
TFunctions
>;
};
}
// Демонстрация строгого конвейера:
const runPipeline = createPipeline<string>();
const result = runPipeline(
" agenticlab_developer ",
(raw) => raw.trim(), // string => string
(clean) => clean.toUpperCase(), // string => string
(str) => str.length, // string => number
(len) => len > 10 // number => boolean
);
// result имеет точный тип 'boolean'!
// ❌ Ошибка компиляции TS2345: попытка передать несовместимую функцию блокируется:
/*
runPipeline(
"test",
(str) => str.length, // string => number
(num: boolean) => !num // ОШИБКА: number не присваивается boolean!
);
*/Что произойдет, если в рекурсивную операцию над кортежами передать открытый массив string[] вместо фиксированного кортежа [string, number]?
Если условный тип не имеет терминальной ветви выхода (например, проверки T extends readonly []), компилятор не сможет остановиться и мгновенно исчерпает стек инстанциации, выдав ошибку TS2589.
// ❌ БЕСКОНЕЧНАЯ РЕКУРСИЯ: нет проверки на пустой кортеж
type BadRecursive<T extends unknown[]> = T extends [infer Head, ...infer Tail]
? [Head, ...BadRecursive<Tail>]
: BadRecursive<T>; // Фатальное зацикливание!
// ✅ ПРАВИЛЬНО: четкое терминальное условие выхода
type GoodRecursive<T extends unknown[]> = T extends [infer Head, ...infer Tail]
? [Head, ...GoodRecursive<Tail>]
: []; // Выход на пустом кортежеПопытка вернуть [...TailRec<Rest>, Current] вместо чистого вызова TailRec<Rest, [Current, ...Acc]> заставляет компилятор откладывать операцию спреда до возврата из функции. Это немедленно отключает оптимизацию TCO и снижает лимит глубины с 1000 до 45 вызовов.
Если передать в операцию над кортежами открытый массив string[] вместо кортежа [string, number], операция Head<string[]> вернет string, а Tail<string[]> вернет string[], полностью разрушая пошаговую рекурсивную декомпозицию длины.
При ручной деструктуризации кортежей с именованными элементами [id: string, count: number] простые операции спреда могут терять метки в подсказках IDE. Используйте явное сохранение меток при необходимости документирования публичных API.