Lesson 49 of 80
Spreading a generic tuple type into another tuple type — the mechanism that lets a curry or concat function keep every argument's exact type instead of collapsing them into `unknown[]`.
14 min read
Every option explains itself — why the right one is right, and why each wrong one is wrong. Miss one and it comes back later on a spaced-repetition schedule.
This topic is about judgment and practical tradeoffs more than a single right answer — a mock interview is the better way to test yourself on it.
Be interviewed on this →Hints without spoilers, and a plain explanation of why a wrong answer was wrong — answered only from this lesson. Free account, no card.
Sign up freeWhat interviewers actually ask about this, and what a strong answer has to cover.
A strong answer covers
Likely follow-ups
You already know rest elements in a tuple: [string, ...number[]] says "a string, then any number of numbers." A variadic tuple type takes that one step further — instead of spreading a fixed type like number, you spread a generic tuple type parameter, so the shape of one tuple gets carried, position by position, into another.
type Prepend<T extends unknown[], Item> = [Item, ...T];
type A = Prepend<[string, number], boolean>;
// [boolean, string, number]
T isn't fixed to a specific tuple here — it's a type parameter that itself is constrained to unknown[]. ...T spreads whatever tuple gets passed in, in order, preserving each element's own type. This is the piece that was missing before TypeScript 4.0: generics could describe "an array of some element type," but not "this exact tuple shape, whatever it turns out to be."
curry is the classic example where this actually earns its keep — turning a function that takes multiple arguments into a chain of one-argument calls, without losing any argument's type along the way:
type Curry<Args extends unknown[], Return> = Args extends [infer First, ...infer Rest]
? (arg: First) => Curry<Rest, Return>
: Return;
function curry<Args extends unknown[], Return>(
fn: (...args: Args) => Return
): Curry<Args, Return> {
// runtime implementation omitted — the type is the interesting part
return undefined as any;
}
function add3(a: number, b: string, c: boolean): string {
return `${a}-${b}-${c}`;
}
const curriedAdd3 = curry(add3);
const result = curriedAdd3(1)("two")(true);
// result: string — and each intermediate call is checked against the real parameter type
Args extends [infer First, ...infer Rest] peels one element off the front of the tuple and recurses on the rest — First is number on the first call, string on the second, boolean on the third, and Rest shrinks by one element each time until it's empty and Curry resolves to the plain Return type. Every step is checked against add3's real signature; passing a number where curriedAdd3 expects a string fails to compile, exactly as if you'd called add3 directly.
[first: string, ...middle: T, last: boolean] is legal as long as there's only one variadic spread per tuple — TypeScript needs to be able to figure out where the fixed trailing elements begin.Function.prototype.bind and concat rely on. Before variadic tuples, bind's built-in type had to approximate the "remaining arguments after the bound ones" with a loose any[]; variadic tuples let the standard library express the real, precise remainder.infer ...Rest inside a conditional type is how you destructure a tuple type recursively — the same [infer First, ...infer Rest] pattern from the curry example generalizes to any "process one element, recurse on what's left" type-level algorithm, not just currying specifically."Why couldn't you type a generic curry function correctly before TypeScript 4.0?" is a good way to surface whether someone understands variadic tuples as solving a real, specific gap — generics could describe "an array of T," but had no way to say "this tuple, whatever shape it has, spread in place" until this feature shipped.