In TypeScript, how do you type a rest parameter, and what changes when you type it as a tuple instead of an array?
answer
- must be last, and only one
- its annotation is array-shaped
- tuple pins the exact arity
- labels are for the call site
- spread needs a known length
basics
~20 sA rest parameter must be typed as an array or a tuple, and must be the last parameter. An array type such as ...args: number[] accepts any number of arguments; a tuple type such as ...args: [x: number, y: number] pins the exact arity and names each position.
solid answer
~50 sA rest parameter collects the remaining arguments, and its declared type has to be an array-shaped type — `...args: string[]` accepts zero or more strings and gives `args` the type `string[]` inside the body. Typing it as a tuple is the more precise option: `function move(...args: [x: number, y: number])` requires exactly two arguments and labels each position, so tooling shows `x` and `y` at the call site while the body still sees a single `args` array. Tuples let you express shapes an array cannot — optional trailing elements with `[a: string, b?: number]`, or a union of tuples such as `[] | [x: number, y: number]` to allow either zero or two arguments. The syntax rules are that a rest parameter must come last and there can be only one. Spreading an array into a fixed-arity call fails, because an array's length is unknown to the compiler.
go deeper
Know that ...args: string[] collects the remaining arguments into an array, that the annotation is the array type rather than the element type, and that the rest parameter has to come last.
Explain what a tuple annotation adds: a fixed arity, per-position types, and labels that show up at the call site — plus optional elements and unions of tuples for shapes an array cannot express.
Show why spread arguments need a known length and how that shapes wrapper and forwarding functions, and note that none of the arity contract survives erasure when calls arrive from untyped code.
Weigh precision against readability: a union-of-tuples signature encodes real call shapes the compiler can enforce, but heavy argument-list typing produces error messages your team has to be able to read. Decide where that complexity is worth carrying.
## The basic form A rest parameter gathers every remaining argument into one binding. Its annotation must be an array-shaped type: ```ts function sum(...values: number[]): number { return values.reduce((a, b) => a + b, 0); } sum(); // ok sum(1, 2, 3); // ok sum(1, 'x'); // error: 'x' is not assignable to number ``` Inside the body `values` is a real `number[]`. If you annotate a rest parameter with a non-array type, the compiler rejects the declaration — a rest parameter must be of an array type. Two positional rules: the rest parameter must be **last**, and there can be only **one**. ```ts function bad(...rest: number[], last: string) {} // error: a rest parameter must be last function ok(first: string, ...rest: number[]) {} // fine ``` ## Tuple-typed rest parameters A tuple is also an array-shaped type, so it is a legal rest annotation — and it is far more precise, because a tuple fixes both the length and the type at each position. ```ts function move(...args: [x: number, y: number]) { const [x, y] = args; } move(1, 2); // ok move(1); // error: Expected 2 arguments, but got 1 move(1, 2, 3); // error: Expected 2 arguments, but got 3 ``` The `x:` and `y:` parts are **labeled tuple elements**. They are documentation for the checker and for editor tooling: the call site shows parameter names as if you had written `move(x: number, y: number)`, while the body still receives one `args` array. Labels are all-or-nothing — if you label one element of a tuple you must label them all. ## Shapes a tuple can express and an array cannot **Optional trailing elements**, which map onto optional arguments: ```ts function log(...args: [message: string, level?: string]) {} log('hi'); log('hi', 'warn'); ``` **A union of tuples**, which expresses "either this exact shape or that one": ```ts function area(...args: [size: number] | [width: number, height: number]) { return args.length === 1 ? args[0] * args[0] : args[0] * args[1]; } area(4); // ok area(4, 5); // ok area(4, 5, 6); // error ``` Inside the body, checking `args.length` narrows the union, because tuple lengths are literal types — that is what makes the two branches type-check. **A tuple with its own rest element**, giving a fixed head and an open tail: ```ts function tag(...args: [name: string, ...values: number[]]) {} tag('t'); tag('t', 1, 2, 3); ``` ## Spreading arguments into a call The complement of a rest parameter is a spread argument. The compiler needs to know the length of what you spread: ```ts function move(x: number, y: number) {} const pair: [number, number] = [1, 2]; move(...pair); // ok — tuple length is known const list: number[] = [1, 2]; move(...list); // error — an array's length is unknown, so arity cannot be checked ``` Spreading an array *is* accepted when the target has a matching rest parameter, since then any length is acceptable: ```ts function sum(...values: number[]) {} sum(...list); // ok ``` ## Where tuple rests earn their keep The pattern shows up wherever a signature has to be built out of another one — a wrapper that forwards its arguments, a function that partially applies some of them, an event map whose payload shape depends on the event name. The tuple form lets the argument list travel as a single type instead of being retyped by hand. Reaching for that machinery is a generics topic; what belongs here is knowing that the rest parameter's type is just an array-shaped type, so anything you can express about arrays and tuples you can express about an argument list. ## Erasure The rest *syntax* is JavaScript and is emitted (`function sum(...values) {}`); the annotation is erased. There is no runtime arity check, so a call arriving from untyped code can pass any number of arguments and the tuple contract will not be enforced. ## Common mistakes - Putting the rest parameter anywhere but last, or declaring two of them. - Annotating a rest parameter with the element type (`...values: number`) instead of the array type. - Expecting `f(...someArray)` to work against a fixed-arity function. - Assuming a tuple-typed rest changes what the body receives — it is still one array binding. - Labeling only some elements of a tuple.
- Why does spreading a `number[]` into a two-parameter function fail while spreading a `[number, number]` succeeds?Arity checking needs a known length. A tuple type carries its length as part of the type, so the compiler can match each element to a parameter. A plain array could hold zero or a hundred elements, so the compiler cannot prove the call supplies exactly the required arguments and reports the spread as unusable there.
- What do labeled tuple elements actually buy you, given that the body still receives one array?They restore parameter names at the call site: editors show `x` and `y` for `...args: [x: number, y: number]` exactly as for ordinary parameters, and error messages reference the label. They carry no runtime meaning and do not change the binding inside the body, which is still a single array.
- How would you type a rest parameter that accepts either no arguments or exactly a pair of numbers?Use a union of tuples: `function f(...args: [] | [x: number, y: number])`. Calls with zero or two numbers check, and anything else is rejected. Inside the body, testing `args.length` narrows the union, because each tuple's length is a literal type.
saying these in an interview costs you the question
- Puts the rest parameter before other parameters
- Annotates the rest parameter with the element type
- Expects spreading a plain array into a fixed-arity call to work
- Thinks a tuple-typed rest gives the body separate bindings
- Believes the arity contract is checked at runtime