Variadic Tuple Types
Use rest elements and spread in tuple types to model typed `compose`, `pipe`, `concat`, and argument-forwarding functions with full type inference.
Overview
Key Points
- •Rest elements in tuple positions: `[string, ...number[], boolean]`
- •Spread in type position: `type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B]`
- •Leading and trailing rests are allowed (not just at the end)
- •Labeled tuple elements: `[name: string, age: number]` - labels appear in IntelliSense
- •Inference through spreads: TypeScript can infer T from `[...T, string]`
- •`Parameters<F>` and `ReturnType<F>` leverage variadic tuples internally
- •Enables: typed `pipe`/`compose`, `concat`, argument forwarding, partial application
Examples
Spreading and concatenating tuple types
// Concat two tuple types
type Concat<A extends unknown[], B extends unknown[]> = [...A, ...B];
type AB = Concat<[string, number], [boolean, string]>;
// [string, number, boolean, string]
// Prepend an element
type Prepend<T, List extends unknown[]> = [T, ...List];
type P = Prepend<string, [number, boolean]>; // [string, number, boolean]
// Append an element
type Append<List extends unknown[], T> = [...List, T];
type A = Append<[number, boolean], string>; // [number, boolean, string]
// Drop the first element
type Tail<T extends unknown[]> = T extends [unknown, ...infer Rest] ? Rest : never;
type T = Tail<[string, number, boolean]>; // [number, boolean]
// Drop the last element
type Init<T extends unknown[]> = T extends [...infer Rest, unknown] ? Rest : never;
type I = Init<[string, number, boolean]>; // [string, number]
// Zip two tuples together
type Zip<A extends unknown[], B extends unknown[]> = {
[K in keyof A]: K extends keyof B ? [A[K], B[K]] : never;
};
type Z = Zip<[string, number], [boolean, Date]>;
// [[string, boolean], [number, Date]]Typed argument forwarding and partial application
// Forward arguments from one function to another
function withLogging<Args extends unknown[], Return>(
fn: (...args: Args) => Return,
label: string
): (...args: Args) => Return {
return (...args: Args) => {
console.log(`[${label}] called with`, args);
const result = fn(...args);
console.log(`[${label}] returned`, result);
return result;
};
}
function add(a: number, b: number): number { return a + b; }
function greet(name: string, greeting: string): string {
return `${greeting}, ${name}!`;
}
const loggedAdd = withLogging(add, 'add');
const loggedGreet = withLogging(greet, 'greet');
loggedAdd(2, 3); // fully typed: (a: number, b: number) => number
loggedGreet('Alice', 'Hi'); // fully typed: (name: string, greeting: string) => string
// Partial application using variadic tuples
function partial<F extends (...args: any[]) => any, PartialArgs extends Partial<Parameters<F>>>(
fn: F,
...partial: PartialArgs
): (...rest: Parameters<F> extends [...PartialArgs, ...infer R] ? R : never) => ReturnType<F> {
return (...rest) => fn(...partial, ...rest);
}
const add5 = partial(add, 5);
console.log(add5(3)); // 8 - only needs the second argumentTyped compose and pipe using variadic tuples
// A type-safe pipe function for two steps
function pipe2<A, B, C>(
f: (a: A) => B,
g: (b: B) => C
): (a: A) => C {
return (a) => g(f(a));
}
const processName = pipe2(
(s: string) => s.trim(),
(s: string) => s.toLowerCase()
);
console.log(processName(' ALICE ')); // "alice"
// Typed concat for arrays using variadic tuples
function concatArrays<A extends unknown[], B extends unknown[]>(
a: [...A],
b: [...B]
): [...A, ...B] {
return [...a, ...b] as [...A, ...B];
}
const nums = [1, 2, 3] as const;
const bools = [true, false] as const;
const combined = concatArrays(nums, bools);
// Type: [1, 2, 3, true, false] - exact literal tuple!
// Labeled tuples for better IDE experience
type NamedPoint = [x: number, y: number, z?: number];
function distance([x1, y1]: NamedPoint, [x2, y2]: NamedPoint): number {
return Math.sqrt((x2 - x1) ** 2 + (y2 - y1) ** 2);
}
const origin: NamedPoint = [0, 0];
const point: NamedPoint = [3, 4];
console.log(distance(origin, point)); // 5Common Mistakes
- Multiple rest elements in the same tuple: TypeScript allows only one rest element per tuple (e.g., `[...A, ...B]` as a spread of two tuples is fine, but `[...A[], ...B[]]` is not valid)
- Using variadic tuples for runtime array operations without proper spread: `[...a, ...b]` at the type level is a type operation; at runtime you still need to spread values correctly
- Overusing labeled tuple elements: labels are purely cosmetic (for IntelliSense display) and have no runtime effect or type enforcement - you can destructure them with any name
Note
Variadic tuples are most valuable for library code that wraps functions generically: middleware systems, argument transformers, decorator chains. For application code, the complexity is usually not justified. Start with explicit overloads for 1-5 cases; reach for variadic tuples when you genuinely need N-ary generics.
Exercises
Write TypeScript code to solve each exercise. Click Run to compile and execute it right here, then mark as done.
Completed: 0 / 2 exercises
Knowledge Check
Question 1 of 4