29. Variadic Tuples
TypeScript: Броня. Урок 28: Variadic Tuple Types
Заголовок раздела «TypeScript: Броня. Урок 28: Variadic Tuple Types»Variadic tuple types (вариадические кортежи), введённые в TypeScript 4.0, позволяют работать с кортежами переменной длины и создавать мощные типы для функций с произвольным количеством аргументов. Это критически важная фича для type-safe работы с rest parameters, spread operators и generic tuple manipulation.
Базовый синтаксис
Заголовок раздела «Базовый синтаксис»// Spread в tuple typestype StringNumber = [string, number];type BooleanStringNumber = [boolean, ...StringNumber];// [boolean, string, number]
// Generic spreadtype Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
type Result1 = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]type Result2 = Concat<[string], [number, boolean]>; // [string, number, boolean]
// Spread в началеtype Prepend<T extends unknown[], U> = [U, ...T];
type WithId = Prepend<[string, number], 'id'>; // ['id', string, number]
// Spread в концеtype Append<T extends unknown[], U> = [...T, U];
type WithTimestamp = Append<[string, number], Date>; // [string, number, Date]Rest Elements в Tuples
Заголовок раздела «Rest Elements в Tuples»// Rest element может быть в начале, середине или концеtype Start = [string, ...number[]]; // string, потом любое количество numbertype Middle = [string, ...boolean[], number]; // string, boolean[], numbertype End = [...string[], number]; // любое количество string, потом number
// Примеры использованияconst start: Start = ['hello', 1, 2, 3, 4, 5];const middle: Middle = ['text', true, false, true, 42];const end: End = ['a', 'b', 'c', 100];
// Generic rest elementstype Tail<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : [];
type Numbers = Tail<[string, number, boolean]>; // [number, boolean]type Empty = Tail<[string]>; // []Type-safe Rest Parameters
Заголовок раздела «Type-safe Rest Parameters»// Функция принимающая произвольное количество аргументовfunction concat<T extends unknown[]>(...args: T): T { return args;}
const result1 = concat(1, 'a', true); // [number, string, boolean]const result2 = concat(); // []const result3 = concat('x', 'y', 'z'); // [string, string, string]
// Prepend к rest parametersfunction logWithPrefix<T extends unknown[]>( prefix: string, ...args: T): void { console.log(prefix, ...args);}
logWithPrefix('Info:', 'User logged in', { userId: 123 });// T = [string, { userId: number }]
// Множественные rest parameters через genericsfunction merge<T extends unknown[], U extends unknown[]>( first: [...T], second: [...U]): [...T, ...U] { return [...first, ...second];}
const merged = merge([1, 2], ['a', 'b']); // [number, number, string, string]Практический пример: Type-safe Curry
Заголовок раздела «Практический пример: Type-safe Curry»// Curry function с полной типизациейtype Curry<T extends unknown[], R> = T extends [infer First, ...infer Rest] ? (arg: First) => Rest extends [] ? R : Curry<Rest, R> : R;
function curry<T extends unknown[], R>( fn: (...args: T) => R): Curry<T, R> { return function curried(...args: unknown[]): any { if (args.length >= fn.length) { return fn(...args as T); }
return (...nextArgs: unknown[]) => curried(...args, ...nextArgs); } as Curry<T, R>;}
// Использованиеfunction add(a: number, b: number, c: number): number { return a + b + c;}
const curriedAdd = curry(add);// (arg: number) => (arg: number) => (arg: number) => number
const result = curriedAdd(1)(2)(3); // 6
// Type-safe на каждом шагеconst step1 = curriedAdd(10); // (arg: number) => (arg: number) => numberconst step2 = step1(20); // (arg: number) => numberconst step3 = step2(30); // number (60)Variadic Generics в Functions
Заголовок раздела «Variadic Generics в Functions»// Generic функция с variadic tuplesfunction pipe<T extends unknown[], U>( value: T, fn: (...args: T) => U): U { return fn(...value);}
const result1 = pipe([1, 2, 3], (a, b, c) => a + b + c); // numberconst result2 = pipe(['hello', 5], (str, num) => str.repeat(num)); // string
// Composition с variadic tuplestype AnyFunction = (...args: any[]) => any;
type LastReturnType<T extends AnyFunction[]> = T extends [ ...any[], infer Last extends AnyFunction] ? ReturnType<Last> : never;
type FirstParameters<T extends AnyFunction[]> = T extends [ infer First extends AnyFunction, ...any[]] ? Parameters<First> : never;
function compose<T extends AnyFunction[]>( ...fns: T): (...args: FirstParameters<T>) => LastReturnType<T> { return (...args: any[]) => { return fns.reduceRight( (acc, fn) => (Array.isArray(acc) ? fn(...acc) : fn(acc)), args ); };}
// Использованиеconst add1 = (x: number) => x + 1;const double = (x: number) => x * 2;const toString = (x: number) => x.toString();
const composed = compose(toString, double, add1);// (x: number) => string
const result = composed(5); // "12" ((5 + 1) * 2).toString()Жизненный пример: Type-safe Event Bus
Заголовок раздела «Жизненный пример: Type-safe Event Bus»// Event bus с variadic payload typestype EventMap = { [key: string]: unknown[];};
class TypedEventBus<TEvents extends EventMap> { private listeners: { [K in keyof TEvents]?: Array<(...args: TEvents[K]) => void>; } = {};
on<K extends keyof TEvents>( event: K, handler: (...args: TEvents[K]) => void ): void { if (!this.listeners[event]) { this.listeners[event] = []; } this.listeners[event]!.push(handler); }
emit<K extends keyof TEvents>( event: K, ...args: TEvents[K] ): void { const handlers = this.listeners[event]; if (handlers) { handlers.forEach(handler => handler(...args)); } }
off<K extends keyof TEvents>( event: K, handler: (...args: TEvents[K]) => void ): void { const handlers = this.listeners[event]; if (handlers) { const index = handlers.indexOf(handler); if (index !== -1) { handlers.splice(index, 1); } } }}
// Определение событий с variadic payloadsinterface AppEvents extends EventMap { 'user:login': [userId: string, timestamp: number, ip: string]; 'user:logout': [userId: string]; 'data:update': [key: string, oldValue: unknown, newValue: unknown]; 'error': [message: string, code: number, stack?: string];}
const bus = new TypedEventBus<AppEvents>();
// Type-safe listenersbus.on('user:login', (userId, timestamp, ip) => { // userId: string, timestamp: number, ip: string console.log(`User ${userId} logged in from ${ip} at ${timestamp}`);});
bus.on('error', (message, code, stack) => { // message: string, code: number, stack: string | undefined console.error(`Error ${code}: ${message}`); if (stack) console.error(stack);});
// Type-safe emitbus.emit('user:login', 'user123', Date.now(), '192.168.1.1'); // ✓// bus.emit('user:login', 'user123', Date.now()); // ✗ отсутствует ipTuple Manipulation Types
Заголовок раздела «Tuple Manipulation Types»// Извлечение первого элементаtype First<T extends unknown[]> = T extends [infer F, ...unknown[]] ? F : never;
type FirstNumber = First<[number, string, boolean]>; // number
// Извлечение последнего элементаtype Last<T extends unknown[]> = T extends [...unknown[], infer L] ? L : never;
type LastBoolean = Last<[number, string, boolean]>; // boolean
// Удаление первого элементаtype Tail<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : [];
type TailResult = Tail<[1, 2, 3, 4]>; // [2, 3, 4]
// Удаление последнего элементаtype Init<T extends unknown[]> = T extends [...infer I, unknown] ? I : [];
type InitResult = Init<[1, 2, 3, 4]>; // [1, 2, 3]
// Reverse tupletype Reverse<T extends unknown[]> = T extends [infer F, ...infer R] ? [...Reverse<R>, F] : [];
type Reversed = Reverse<[1, 2, 3, 4]>; // [4, 3, 2, 1]
// Concat tuplestype Concat<T extends unknown[], U extends unknown[]> = [...T, ...U];
type Combined = Concat<[1, 2], [3, 4, 5]>; // [1, 2, 3, 4, 5]
// Flatten nested tuplestype Flatten<T extends unknown[]> = T extends [infer F, ...infer R] ? F extends unknown[] ? [...Flatten<F>, ...Flatten<R>] : [F, ...Flatten<R>] : [];
type Flat = Flatten<[1, [2, 3], [4, [5, 6]]]>; // [1, 2, 3, 4, [5, 6]]Type-safe Promisify
Заголовок раздела «Type-safe Promisify»// Promisify функций с variadic argumentstype Callback<T extends unknown[]> = (error: Error | null, ...result: T) => void;
type PromisifyFunction<T extends unknown[], R extends unknown[]> = (...args: [...T, Callback<R>]) => void;
type Promisified<T extends unknown[], R extends unknown[]> = (...args: T) => Promise<R extends [infer Single] ? Single : R>;
function promisify<T extends unknown[], R extends unknown[]>( fn: PromisifyFunction<T, R>): Promisified<T, R> { return (...args: T): Promise<any> => { return new Promise((resolve, reject) => { fn(...args, (error, ...result) => { if (error) { reject(error); } else { resolve(result.length === 1 ? result[0] : result); } }); }); };}
// Использованиеdeclare function readFile( path: string, encoding: string, callback: (error: Error | null, data: string) => void): void;
const readFileAsync = promisify(readFile);// (path: string, encoding: string) => Promise<string>
const content = await readFileAsync('/path/to/file', 'utf-8'); // stringPartial Tuples
Заголовок раздела «Partial Tuples»// Partial для tuplestype PartialTuple<T extends unknown[]> = Partial<T> & unknown[];
type OptionalNumbers = PartialTuple<[number, number, number]>;// (number | undefined)[]
// Required для tuplestype RequiredTuple<T extends unknown[]> = Required<T> & unknown[];
// Readonly для tuplestype ReadonlyTuple<T extends unknown[]> = Readonly<T>;
const tuple: ReadonlyTuple<[number, string]> = [42, 'hello'];// tuple[0] = 100; // ✗ readonlyMapped Types с Variadic Tuples
Заголовок раздела «Mapped Types с Variadic Tuples»// Transform каждого элемента tupletype MapTuple<T extends unknown[], F> = { [K in keyof T]: T[K] extends infer U ? F extends (arg: U) => infer R ? R : never : never;};
// Пример: обёртка в Promisetype PromisifyAll<T extends unknown[]> = { [K in keyof T]: Promise<T[K]>;};
type AsyncTuple = PromisifyAll<[number, string, boolean]>;// [Promise<number>, Promise<string>, Promise<boolean>]
// Nullable tupletype NullableTuple<T extends unknown[]> = { [K in keyof T]: T[K] | null;};
type MaybeNull = NullableTuple<[string, number]>;// [string | null, number | null]Ключевые моменты
Заголовок раздела «Ключевые моменты»- Variadic tuple types позволяют работать с кортежами переменной длины
- Spread operator (
...) работает в типах так же, как в JavaScript - Rest elements могут быть в начале, середине или конце tuple
- Критически важны для type-safe rest/spread parameters
- Позволяют создавать точно типизированные curry, compose, pipe
- Используются для manipulation tuple types (reverse, concat, flatten)
- Поддерживают generic constraints и inference
- Mapped types работают с variadic tuples
- Необходимы для создания type-safe event systems, HOF, utilities
- Доступны с TypeScript 4.0+