У ранніх версіях 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 без очікування стека! |
+-------------------------------------------------------------------------+У хвостовій рекурсії умовний тип повертає безпосередній виклик себе самого без огортання його в додаткові типи (кортежі, об'єднання чи перетини). Це дозволяє компілятору повторно використовувати поточний фрейм обчислення.
Компілятор валідує 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!
);
*/Якщо умовний тип не має термінальної гілки виходу (наприклад, перевірки 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.
Створюється початковий акумулятор (за замовчуванням порожній кортеж Acc = []).
Через [infer Head, ...infer Tail] вилучається перший елемент кортежу.
Вилучений елемент поміщається в акумулятор [Head, ...Acc].
Компілятор перезаписує поточний контекст новими значеннями Tail та Acc без алокації стека.
Коли вхідний кортеж порожній, рекурсія завершується, повертаючи накопичений акумулятор Acc.