Вариативные кортежи (Variadic Tuple Types) в TypeScript

28 сентября 2026
Автор

Антон Ларичев

Что такое вариативные кортежи

Вариативные кортежи (Variadic Tuple Types) — возможность TypeScript, появившаяся в версии 4.0, которая позволяет использовать обобщённые spread-выражения внутри типов кортежей. До этой версии кортежи были жёстко фиксированы: нельзя было описать тип, принимающий произвольный набор элементов и при этом сохраняющий информацию о типах каждого из них.

Кортеж в TypeScript — это массив с фиксированной длиной и известными типами на каждой позиции:

type Point = [number, number];
type Named = [string, number, boolean];

До появления вариативных кортежей операции вроде "добавить элемент в начало кортежа" или "склеить два кортежа" нельзя было типизировать обобщённым образом. Вариативные кортежи решают именно эту задачу.

Курс по теме

Курс TypeScript с нуля с нуля

40 000+ студентов · рейтинг 4.8 · гарантия возврата 30 дней

Синтаксис: spread внутри кортежа

Ключевая идея — возможность применить оператор ... к обобщённому параметру-типу прямо внутри кортежного типа:

type Prepend<T, Arr extends unknown[]> = [T, ...Arr];
type Append<Arr extends unknown[], T> = [...Arr, T];

Здесь ...Arr — это spread обобщённого параметра, ограниченного unknown[]. TypeScript понимает, что результирующий кортеж начинается с T, а затем идут все элементы Arr в том же порядке.

Пример использования:

type WithId<T extends unknown[]> = [id: string, ...T];

type UserArgs = [name: string, age: number];
type UserWithId = WithId<UserArgs>;
// => [id: string, name: string, age: number]

TypeScript вычисляет итоговый тип статически — никакой потери информации.

Конкатенация кортежей

Один из самых очевидных примеров применения — типизация функции concat:

function concat<A extends unknown[], B extends unknown[]>(
  a: [...A],
  b: [...B]
): [...A, ...B] {
  return [...a, ...b];
}

const result = concat([1, 'hello'], [true, 42]);
// result: [number, string, boolean, number]

console.log(result); // [1, 'hello', true, 42]

Обратите внимание на запись параметров как [...A] вместо просто A. Такая запись подсказывает TypeScript, что нужно выводить тип как кортеж, а не как обычный массив. Без неё вывод типа может потерять информацию о позициях:

// Без [...A] — TypeScript выводит массивы, теряя позиции
function concatBad<A extends unknown[], B extends unknown[]>(
  a: A,
  b: B
): [...A, ...B] {
  return [...a, ...b];
}

const r1 = concatBad([1, 'hello'], [true]);
// r1: (string | number | boolean)[]  — потеря информации о позициях

// С [...A] — TypeScript сохраняет позиционную информацию
const r2 = concat([1, 'hello'], [true]);
// r2: [number, string, boolean]  — точный тип

Добавление и удаление элементов

Вариативные кортежи позволяют описать операции над структурой кортежа на уровне типов:

// Добавить элемент в начало
type Prepend<Head, Tail extends unknown[]> = [Head, ...Tail];

// Добавить элемент в конец
type Append<Init extends unknown[], Last> = [...Init, Last];

// Получить первый элемент
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;

// Получить все элементы кроме первого
type Tail<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : never;

// Получить последний элемент
type Last<T extends unknown[]> = T extends [...unknown[], infer L] ? L : never;

// Получить все элементы кроме последнего
type Init<T extends unknown[]> = T extends [...infer I, unknown] ? I : never;

// Примеры
type A = Head<[string, number, boolean]>;  // string
type B = Tail<[string, number, boolean]>;  // [number, boolean]
type C = Last<[string, number, boolean]>;  // boolean
type D = Init<[string, number, boolean]>;  // [string, number]

Это фундаментальные строительные блоки для более сложных манипуляций с типами.

Остаточные элементы в кортежах

Раньше в TypeScript остаточные элементы (...rest) могли стоять только в конце кортежа. Начиная с TypeScript 4.0 ограничение частично снято: rest-элемент может стоять в любом месте, если остальные элементы — фиксированные.

// Rest в начале
type StringsAndNumber = [...string[], number];
const a: StringsAndNumber = [1];              // OK
const b: StringsAndNumber = ['a', 'b', 1];    // OK
const c: StringsAndNumber = ['a', 'b'];       // Ошибка — нет финального number

// Rest в середине
type Sandwich<T> = [string, ...T[], boolean];
const s1: Sandwich<number> = ['start', true];           // OK
const s2: Sandwich<number> = ['start', 1, 2, 3, true];  // OK

Ограничение: в одном кортеже может быть не более одного rest-элемента. TypeScript не умеет разрешать два произвольных rest-элемента одновременно, так как это привело бы к неоднозначности при выводе типов.

// Ошибка: два rest-элемента
type Invalid = [...string[], ...number[]]; // TS2574: rest element type must be an array type

Именованные элементы кортежей

TypeScript 4.0 также добавил поддержку именованных элементов кортежей (labeled tuple elements). Они не влияют на систему типов, но улучшают читаемость и подсказки в IDE:

type Range = [start: number, end: number];
type HttpRequest = [method: string, url: string, body?: unknown];

function makeRange(start: number, end: number): Range {
  return [start, end];
}

Важное правило: если один элемент именован, все остальные тоже должны быть именованы. Смешивать именованные и безымянные нельзя:

// Ошибка: нельзя миксовать именованные и безымянные
type Mixed = [string, end: number]; // TS4022

// Корректно
type Correct = [start: string, end: number];

Имена в вариативных контекстах сохраняются при разворачивании:

type EventPayload<T extends unknown[]> = [type: string, ...T];
type ClickEvent = EventPayload<[x: number, y: number]>;
// => [type: string, x: number, y: number]

Практический пример: типизация curry

Одна из классических задач, где вариативные кортежи незаменимы — типизация функций с частичным применением аргументов (currying):

type PartialApply<
  Params extends unknown[],
  Applied extends unknown[]
> = Params extends [...Applied, ...infer Rest] ? Rest : never;

function partial<F extends (...args: any[]) => any>(
  fn: F,
  ...applied: PartialApply<Parameters<F>, []>
) {
  return (...rest: PartialApply<Parameters<F>, typeof applied>) =>
    fn(...applied, ...rest);
}

function add(a: number, b: number, c: number): number {
  return a + b + c;
}

const addFive = partial(add, 5);
// addFive: (b: number, c: number) => number

console.log(addFive(3, 2)); // 10

TypeScript точно вычитает, какие аргументы уже "зафиксированы", а какие ещё нужно передать.

Практический пример: типизация middleware

Вариативные кортежи хорошо подходят для описания цепочек обработчиков, где каждый обработчик расширяет контекст:

type Middleware<In, Out> = (ctx: In) => Out;

type Pipeline<
  Middlewares extends Middleware<any, any>[]
> = Middlewares extends []
  ? Middleware<unknown, unknown>
  : Middlewares extends [Middleware<infer In, infer Out>]
  ? Middleware<In, Out>
  : Middlewares extends [
      Middleware<infer In, infer Mid>,
      ...infer Rest extends Middleware<any, any>[]
    ]
  ? Pipeline<[Middleware<Mid, any>, ...Rest]> extends Middleware<any, infer FinalOut>
    ? Middleware<In, FinalOut>
    : never
  : never;

// Упрощённый вариант для compose
function compose<A, B, C>(
  f: (a: A) => B,
  g: (b: B) => C
): (a: A) => C {
  return (a) => g(f(a));
}

const parseNumber = (s: string): number => parseInt(s, 10);
const double = (n: number): number => n * 2;
const toString = (n: number): string => `Result: ${n}`;

const pipeline = compose(compose(parseNumber, double), toString);
console.log(pipeline('21')); // 'Result: 42'

Практический пример: функция zip

Функция zip, объединяющая несколько массивов в массив кортежей, требует вариативных типов для корректной типизации:

function zip<T extends unknown[][]>(
  ...arrays: { [K in keyof T]: T[K] extends (infer V)[] ? V[] : never }
): { [K in keyof T]: T[K] extends (infer V)[] ? V : never }[] {
  const length = Math.min(...arrays.map((a) => a.length));
  return Array.from({ length }, (_, i) =>
    arrays.map((arr) => arr[i])
  ) as any;
}

const names = ['Alice', 'Bob', 'Charlie'];
const ages = [30, 25, 35];
const scores = [98, 87, 92];

const zipped = zip(names, ages, scores);
// zipped: [string, number, number][]

zipped.forEach(([name, age, score]) => {
  console.log(`${name}: age ${age}, score ${score}`);
});
// Alice: age 30, score 98
// Bob: age 25, score 87
// Charlie: age 35, score 92

Взаимодействие с Parameters и ReturnType

Вариативные кортежи хорошо работают вместе со встроенными утилитными типами Parameters и ReturnType:

function fetchUser(id: number, includeRoles: boolean): Promise<{ name: string }> {
  return fetch(`/users/${id}?roles=${includeRoles}`).then((r) => r.json());
}

// Parameters возвращает кортеж
type FetchUserParams = Parameters<typeof fetchUser>;
// => [id: number, includeRoles: boolean]

// Можно расширить
type WithRequestId<T extends unknown[]> = [requestId: string, ...T];
type FetchUserWithRequestId = WithRequestId<FetchUserParams>;
// => [requestId: string, id: number, includeRoles: boolean]

// Обёртка с трассировкой
function withTracing<F extends (...args: any[]) => any>(
  fn: F,
  name: string
): (...args: Parameters<F>) => ReturnType<F> {
  return (...args) => {
    console.log(`[${name}] called with`, args);
    const result = fn(...args);
    console.log(`[${name}] returned`, result);
    return result;
  };
}

const tracedFetch = withTracing(fetchUser, 'fetchUser');
// tracedFetch: (id: number, includeRoles: boolean) => Promise<{ name: string }>

Ограничения

Несмотря на мощь вариативных кортежей, у них есть ряд ограничений:

Один rest-элемент. В кортеже может быть только один spread неизвестной длины. Два обобщённых spread в одном кортеже TypeScript разрешить не может.

// Нельзя
type TwoSpreads<A extends unknown[], B extends unknown[]> = [...A, ...B]; // OK только если A или B — конкретный тип

Рекурсивные типы требуют осторожности. Глубокая рекурсия с кортежами может исчерпать лимит вычислений TypeScript и вызвать ошибку Type instantiation is excessively deep.

// Такой тип может вызвать проблемы при большой глубине рекурсии
type Flatten<T extends unknown[]> =
  T extends [infer Head, ...infer Tail]
    ? Head extends unknown[]
      ? [...Flatten<Head>, ...Flatten<Tail>]
      : [Head, ...Flatten<Tail>]
    : [];

Вывод типов при mixed rest. Когда rest-элемент стоит не в конце, TypeScript иногда выводит менее точные типы.

type WithHeader<T extends unknown[]> = [string, ...T, number];

function process<T extends unknown[]>(...args: WithHeader<T>): T {
  const [, ...middle] = args;
  return middle.slice(0, -1) as T; // TypeScript не может точно отследить
}

Итог

Вариативные кортежи — инструмент для работы с типами переменной длины без потери позиционной информации. Они особенно полезны при типизации:

  • функций высшего порядка (concat, zip, compose)
  • частичного применения и каррирования
  • декораторов и middleware-цепочек
  • утилитных типов, манипулирующих структурой кортежей

Знание вариативных кортежей позволяет писать библиотечный код с полной типобезопасностью там, где раньше приходилось прибегать к any или перегрузкам функций.

Чтобы глубоко освоить систему типов TypeScript, включая продвинутые техники работы с обобщёнными типами и утилитными типами, приходите на курс по TypeScript: TypeScript курс на PurpleSchool

Utility Types в TypeScript — полный разбор встроенных утилитСтрелочка вправо

Постройте личный план изучения Typescript до уровня Middle — бесплатно!

Typescript — часть карты развития Frontend

  • step100+ шагов развития
  • lessons30 бесплатных лекций
  • lessons300 бонусных рублей на счет

Бесплатные лекции

Все гайды по Typescript

Переменные и константы в TypeScriptНастройка типов с declare module в TypeScriptTypeScript: ключевое слово using и Explicit Resource ManagementTypeScript типизация в Node.js проектеTypeScript ESLint — настройка линтера для проектаTypeScript 5: новые возможности и измененияКортежи в TypeScripttsconfig.json — полный разбор настроек TypeScript компилятораПреобразование к типу в TypeScript (Type Assertion)TypeScript strict и strictNullChecks — строгий режимОбъекты в TypeScriptКак собрать TypeScript проект с RollupMonorepo с Turborepo и TypeScriptФункции в TypeScriptТип функции и стрелочные функции в TypeScriptTypeScript с tRPC — типобезопасный APIТипизация ошибок и обработка исключений в TypeScriptTypeScript ключевое слово declareМассивы в TypeScript
Как правильно использовать тип UnknownИспользование Union в TypeScriptTypeScript с GraphQL — типизация и кодогенерацияType Guards в TypeScript — как работает сужение типовТипы данных в TypeScriptSymbols в TypeScriptСтруктурная типизация в TypeScriptNull и Undefined в TypeScriptТип never в TypeScriptСужение типов (Narrowing) в TypeScriptПонимание Literal Types. Ключевые аспекты и примеры использованияОператор infer в Typescript?Типы функций в TypeScript. Руководство для разработчиковTypeScript с Express.js: типизация маршрутов и middlewareДискриминированные объединения в TypeScriptEnums в TypeScriptСлужебный тип Awaited в TypeScriptКак типизировать массивы TypeScriptКак правильно использовать тип any
Открыть базу знаний

Лучшие курсы по теме

изображение курса

TypeScript с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.8
3 999 ₽ 6 990 ₽
Подробнее
изображение курса

CSS Flexbox

Антон Ларичев
Гарантия
Бонусы
иконка звёздочки рейтинга4.9
бесплатно
Подробнее
изображение курса

Next.js - с нуля

Антон Ларичев
AI-тренажерыAI-тренажеры
Практика в студииПрактика в студии
Гарантия
Бонусы
иконка звёздочки рейтинга4.7
3 999 ₽ 6 990 ₽
Подробнее

Отправить комментарий