In TypeScript, a function `move(x: number, y: number)` is called as `move(...args)`. Why does that fail to compile when `args` is `number[]`, and what type of `args` makes it work?
answer
- the checker is counting arguments
- an array's length is unknown to the type
- a tuple names its own length
- or spread into a rest parameter
- forwarding a whole parameter list
basics
~20 sA number[] has unknown length, so the compiler cannot prove the call supplies exactly two arguments and reports that a spread argument must have a tuple type or be passed to a rest parameter. Typing args as the tuple [number, number] fixes the arity and the call compiles.
solid answer
~40 sSpreading into a fixed-arity call requires the compiler to know how many arguments arrive. `number[]` carries no length information — it could be empty or hold ten items — so the checker refuses, with the error that a spread argument must either have a tuple type or be passed to a rest parameter. Give `args` a tuple type, `[number, number]`, and the length and per-position types are both known, so `move(...args)` type-checks and each element is matched against the corresponding parameter. The other half of the same idea is the receiving side: you can type a rest parameter with a tuple, `function move(...args: [x: number, y: number])`, which produces a two-parameter signature. Together these let a whole argument list travel as one type — the basis of typed forwarding wrappers.
code
typescript · 17 linesfunction move(x: number, y: number): void {
console.log(x, y);
}
const loose = [1, 2]; // inferred number[]
// move(...loose); // Error: spread argument needs a tuple type
const fixed: [number, number] = [1, 2];
move(...fixed); // ok — arity is known
// A rest parameter typed with a tuple: one type for the whole list.
function logged(...args: [x: number, y: number]): void {
console.log('move', args);
move(...args);
}
logged(3, 4);go deeper
Know that spreading an array into a call with fixed parameters is rejected, and that annotating the value with a tuple type such as [number, number] is what makes it compile.
Explain the mechanism: the check is about arity, an array type carries no length, a tuple does, and a rest parameter sidesteps the question entirely by accepting any count.
Use tuple-typed rest parameters to forward signatures without restating them, and recognise that an as assertion on a spread argument trades a compile error for a runtime undefined.
Own the convention for wrapper and middleware layers: signatures expressed once as tuple types and forwarded, rather than duplicated parameter lists that drift apart across a codebase.
## The error and what it is protecting ```typescript function move(x: number, y: number): void { /* ... */ } const args = [1, 2]; // inferred as number[] // move(...args); // Error: A spread argument must either have a tuple type // or be passed to a rest parameter. ``` JavaScript is perfectly happy to run this — spread simply expands whatever elements are there, and missing arguments become `undefined`. TypeScript refuses because it is checking **arity**, and `number[]` tells it nothing about arity. The array may hold zero elements, in which case `x` and `y` are both `undefined` at runtime despite being typed `number`; it may hold five, silently discarding three. Rather than guess, the compiler asks for a type that names the length. ## The fix: a tuple type at the call site ```typescript const fixed: [number, number] = [1, 2]; move(...fixed); // ok ``` Now the checker knows there are exactly two elements and what each one's type is, so it can match element 0 against `x` and element 1 against `y`. The same works for a value that got a tuple type any other way — a function's declared tuple return type, a tuple-typed field, or a variable annotated as above. Optional elements are handled too. Spreading a `[number, number?]` into a function whose second parameter is optional is fine, because both the minimum and maximum sizes are known and both fit the signature. ## The other side: a rest parameter typed with a tuple The error message names a second escape hatch — "or be passed to a rest parameter". A rest parameter accepts any number of arguments, so spreading an open-ended array into it is safe: ```typescript function sum(...values: number[]): number { return values.reduce((a, b) => a + b, 0); } const nums: number[] = [1, 2, 3]; sum(...nums); // ok — the receiver imposes no arity ``` More interesting is typing a rest parameter *with a tuple*, which turns a tuple into a full parameter list: ```typescript type MoveArgs = [x: number, y: number]; function moveT(...args: MoveArgs): void { const [x, y] = args; console.log(x, y); } moveT(1, 2); // called like a normal two-parameter function // moveT(1); // Error: expected 2 arguments, got 1 ``` To callers, `moveT` looks and behaves exactly like `move`. The difference is that its parameter list now exists as a *type* that other code can reference, pass around, and spread. ## Why this pairing matters: forwarding The combination — a tuple flowing into a rest parameter, then spread back out into the target call — is how you write a wrapper that preserves a signature without restating it: ```typescript type Args = [x: number, y: number]; function logged(...args: Args): void { console.log('move', args); move(...args); // legal: args has a tuple type } ``` Inside the wrapper, `args` has the tuple type, so spreading it into `move` satisfies the arity check. Without tuple types the only way to write this is to list the parameters again and keep the two lists in sync by hand. ## The runtime picture None of this survives compilation. `move(...args)` emits as `move(...args)` and JavaScript expands the array at call time. The tuple type is the compiler's evidence for a check it performs before erasing everything; if the value actually has a different length at runtime — because it came from `JSON.parse`, or from an `as [number, number]` assertion — the call still runs, and the mismatch shows up as `undefined` inside the function rather than as an error at the call. ## Common mistakes The first is assuming a two-element array literal is already a tuple. Assigning `const args = [1, 2]` widens to `number[]`; the tuple shape comes from an annotation or an explicit tuple-typed context, not from counting the literal's elements. The second is reaching for `move(...(args as [number, number]))` when the array's real length is not guaranteed — the assertion silences the checker without making the claim true, which is precisely the arity bug the error was trying to prevent. The honest fix is to give the value a tuple type where it is *created*, or to check its length at runtime before asserting.
- Why is spreading a `number[]` into a rest parameter allowed when spreading it into a fixed-arity call is not?A rest parameter accepts any number of arguments, so there is no arity claim to verify — an array of unknown length can never violate it. A fixed parameter list has a required count, and `number[]` gives the compiler no evidence about the count, so it refuses rather than assume.
- Would `move(...(args as [number, number]))` be a reasonable fix?Only when you have independently established the length. An assertion tells the compiler to believe a claim; it emits no check, so if the array really holds one element the call still runs and the second parameter is `undefined` inside a function that declared it `number`. Prefer giving the value a tuple type where it is created, or checking `length` first.
- How does typing a rest parameter with a tuple help a wrapper function?It lets the wrapper name the whole parameter list as one type: the arguments arrive as a tuple, can be logged or transformed as an array, and can be spread back into the wrapped call because the tuple carries the arity. Without it you must restate every parameter and keep the two lists in sync manually.
saying these in an interview costs you the question
- Says a two-element array literal is already a tuple
- Thinks the error is about element types rather than arity
- Reaches for an `as` assertion to silence the arity error
- Believes spread arity is checked at runtime
- Assumes rest parameters and fixed parameters check spreads identically