Introduction
Variadic tuple types, introduced in TypeScript 4.0, let you spread generic type parameters into tuple positions. This enables type-safe operations like concatenating tuples, prepending/appending elements, and building functions that accept variable-length typed argument lists. They are the tuple equivalent of rest parameters at the type level.
Key Concepts
- Spread in tuples:
[...T]whereTis a generic tuple type spreads its elements into the containing tuple. - Tuple concatenation:
[...A, ...B]concatenates two tuple types. - Labeled tuple elements: TypeScript preserves parameter names in tuple types, improving IDE hints.
Real World Context
Variadic tuples power type-safe function composition (pipe, compose), argument forwarding (wrappers, decorators), event emitter APIs (typed emit and on methods), and builder patterns that accumulate typed arguments. They are the foundation of libraries like ts-toolbelt and type-fest's tuple utilities.
Deep Dive
Basic Tuple Spread
You can spread generic tuple types into new tuples:
typescripttype Strings = [string, string]; type Numbers = [number, number]; type Combined = [...Strings, ...Numbers]; // [string, string, number, number]
The spread operator ... works at the type level just like it does at the value level.
Generic Tuple Concatenation
Combining spreads with generics creates reusable tuple operations:
typescripttype Concat<A extends any[], B extends any[]> = [...A, ...B]; type Result = Concat<[1, 2], [3, 4]>; // [1, 2, 3, 4]
This is a type-level implementation of array concatenation.
Prepend and Append
Adding elements to the start or end of a tuple:
typescripttype Prepend<T, Tuple extends any[]> = [T, ...Tuple]; type Append<Tuple extends any[], T> = [...Tuple, T]; type A = Prepend<boolean, [string, number]>; // [boolean, string, number] type B = Append<[string, number], boolean>; // [string, number, boolean]
These simple types are building blocks for more complex tuple manipulation.
Extracting Parts with Infer
Variadic tuples work with infer to destructure tuples:
typescript// Remove the first element type Tail<T extends any[]> = T extends [any, ...infer Rest] ? Rest : []; // Remove the last element type Init<T extends any[]> = T extends [...infer Rest, any] ? Rest : []; // Get the first element type Head<T extends any[]> = T extends [infer First, ...any[]] ? First : never; // Get the last element type Last<T extends any[]> = T extends [...any[], infer L] ? L : never; type T = [string, number, boolean]; type A = Tail<T>; // [number, boolean] type B = Init<T>; // [string, number] type C = Head<T>; // string type D = Last<T>; // boolean
The ...infer Rest pattern captures "the rest of the tuple" as a new tuple type.
Type-Safe Function Wrappers
Variadic tuples enable functions that forward arguments with full type safety:
typescriptfunction withLogging<Args extends any[], R>( fn: (...args: Args) => R, ...args: Args ): R { console.log("Calling with:", args); const result = fn(...args); console.log("Result:", result); return result; } function add(a: number, b: number): number { return a + b; } withLogging(add, 1, 2); // Fully typed: Args = [number, number], R = number withLogging(add, "1", 2); // Error: string is not assignable to number
The Args extends any[] generic captures the exact tuple of parameter types, preserving full type checking through the wrapper.
Recursive Tuple Operations
Combining variadic tuples with recursion enables operations like Reverse:
typescripttype Reverse<T extends any[]> = T extends [infer First, ...infer Rest] ? [...Reverse<Rest>, First] : []; type Original = [1, 2, 3, 4]; type Reversed = Reverse<Original>; // [4, 3, 2, 1]
Each recursion step takes the first element and appends it to the end of the reversed rest.
Common Pitfalls
- Confusing
T[]with[...T]—T[]is an array of elements of typeT.[...T]whereT extends any[]spreads a tuple type. They are structurally different. - Recursion depth limits on large tuples — Recursive tuple operations hit TypeScript's depth limit around 25-50 levels. For larger tuples, consider tail-call optimized patterns or avoid recursion.
Best Practices
- Use variadic tuples for type-safe argument forwarding — Whenever you write a wrapper or decorator function, use
(...args: Args)with a genericArgs extends any[]to preserve the wrapped function's signature. - Prefer built-in tuple operations over recursive ones — TypeScript's built-in spread
[...A, ...B]is more efficient than custom recursive concatenation.
Summary
- Variadic tuples use
[...T]to spread generic tuple types. - They enable type-safe concatenation, prepend, append, and extraction.
- Combined with
infer, they destructure tuples into head, tail, and rest. - They are essential for type-safe function wrappers that forward arguments.
Code Examples
// Type-safe pipe function using variadic tuples
type Last<T extends any[]> = T extends [...any[], infer L] ? L : never;
function pipe<A, B>(a: A, fn1: (a: A) => B): B;
function pipe<A, B, C>(a: A, fn1: (a: A) => B, fn2: (b: B) => C): C;
function pipe<A, B, C, D>(a: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D): D;
function pipe(value: any, ...fns: Function[]): any {
return fns.reduce((acc, fn) => fn(acc), value);
}
// Fully typed pipeline
const result = pipe(
" Hello World ",
(s: string) => s.trim(), // string
(s: string) => s.toLowerCase(), // string
(s: string) => s.split(" "), // string[]
);
// result: string[]