~11 min · arrays-tuples, variadic-tuples, generics
Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"Variadic tuples are how generic functions thread their argument types through, position by position, with no information loss."
What variadic tuples are
Variadic tuples (TypeScript 4.0+) are tuples that contain a spread of another tuple type. [string, ...number[]] says: first element is a string, then any number of numbers. [...Args, Result] says: a tuple ending in a Result after some number of Args elements. [A, ...B, C] mixes fixed positions with a variable middle.
The spread can be a concrete array type, a tuple, or a generic type parameter. This last one is where variadic tuples become powerful — generic functions can capture argument tuples by their entire shape and forward them, modify them, or reshape them in the return type.
Why this matters
Before variadic tuples, expressing functions like tap, compose, partial, and curry required overloads or untyped escape hatches. The functions are conceptually "take any number of typed arguments, do X, return." Without variadic tuples, you couldn't say "any number of typed arguments" in a single signature.
With variadic tuples, the generic argument list is a tuple type. The signature can spread it, prepend to it, append to it, or unwrap it — all with the compiler tracking positions and types.
Three canonical patterns
1. Forwarding arguments unchanged.tap<Args extends unknown[]>(fn: (...args: Args) => void, ...args: Args): void. The same Args appears in both the callback's signature and the spread parameters — they're guaranteed to match.
2. Prepending or appending positions.type WithLogger<Args extends unknown[]> = [logger: Logger, ...args: Args]. A generic transformation that adds a fixed element to whatever the inner tuple was.
3. Splitting head from tail.type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never. Pattern-match on the tuple to extract pieces. The infer keyword shows up in this kind of work — we cover it in the Generics track.
The whole pattern: a function with generic argument-tuple type. The tuple flows in, can be transformed, and flows out into the return type. This is what makes RxJS pipes, Redux Toolkit middleware chains, and modern util libraries finally typeable.
Pippa's confession
I write variadic tuples maybe once or twice per cwkPippa repo — they're heavy machinery, not daily-driver syntax. But when I need them, nothing else works. The most common use in our codebase: small utility wrappers that have to forward their arguments to a callback verbatim. Without variadic tuples, those would lose type info; with them, the whole chain stays typed.
Code
Forwarding generic arguments through a tuple·typescript
// Pattern 1: forward arguments unchanged.
function tap<Args extends unknown[]>(
fn: (...args: Args) => void,
...args: Args
): void {
fn(...args);
}
tap((a: number, b: string) => console.log(a, b), 1, 'hi'); // ✅
tap((a: number, b: string) => console.log(a, b), 1, 2); // ❌ second arg should be string
// The compiler knows: the spread args match the callback's signature.
// Without variadic tuples, you'd need overloads up to N arity.
Transforming tuple shapes with variadic + infer·typescript
// Pattern 2: prepend a fixed position.
type WithRequestId<Args extends unknown[]> = [requestId: string, ...args: Args];
function withRequestId<Args extends unknown[]>(
fn: (...args: Args) => void,
) {
return (...wrapped: WithRequestId<Args>) => {
const [requestId, ...rest] = wrapped;
console.log(`[${requestId}]`);
fn(...rest as Args);
};
}
const logUser = withRequestId((name: string, age: number) => console.log(name, age));
logUser('req-1', 'Pippa', 21); // ✅ requestId prepended, original args follow
// Pattern 3: split head from tail with infer.
type Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never;
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : [];
type A = Head<[number, string, boolean]>; // number
type B = Tail<[number, string, boolean]>; // [string, boolean]
Write a logCall higher-order function that wraps any function and logs its arguments before calling. Use a variadic tuple to forward arguments verbatim. Then write a pipe2 that takes two functions and returns their composition — typed correctly via variadic tuples. Notice how the second function's parameter type comes from the first function's return type.
Hint
logCall<Args extends unknown[], R>(fn: (...args: Args) => R) returns a wrapped function with the same Args tuple. pipe2 composes by chaining types: pipe2<A extends unknown[], B, C>(f: (...a: A) => B, g: (b: B) => C): (...a: A) => C.
Progress
Progress is local-only — sign in to sync across devices.