TypeScript Rest Parameters

A rest parameter lets a function accept an unlimited number of arguments and collects them into a single array-like variable inside the function body. JavaScript has supported rest parameters since ES2015, but TypeScript adds a crucial layer on top: it lets you declare exactly what type those extra arguments must be, so the compiler can catch mismatched calls before your code ever runs. This lesson covers how to type rest parameters as arrays, as precise tuples, how the type checker validates calls against them, and the mistakes beginners commonly make.

Overview: How Rest Parameters Work in TypeScript

In plain JavaScript, function sum(...numbers) { } gathers every argument passed after the fixed parameters into the array numbers. TypeScript requires (and lets you specify) a type for that array, most commonly an array type like number[] or string[]. Once typed, every argument the caller passes into that rest slot is checked against the element type — pass a string where a number is expected and tsc reports an error immediately, long before the function ever executes.

A rest parameter must always be the last parameter in the parameter list, and its type must be an array type or a tuple type — never a plain scalar type like number or string. If you omit the type annotation entirely, TypeScript tries to infer it from context (for example, from an expected function type in a callback position); if it can’t infer anything and noImplicitAny is on (which --strict enables), you’ll get an implicit-any error, so in practice you should always annotate rest parameters explicitly.

TypeScript also supports tuple types for rest parameters, which is where things get powerful: instead of saying “any number of numbers,” you can say “at least one string, followed by any number of unknown values,” using a variadic tuple type like [string, ...unknown[]]. This lets you encode much stricter call signatures than a plain array type ever could.

Remember that all of this checking happens purely at compile time. TypeScript’s type system is erased when the code compiles to JavaScript — the emitted JavaScript has no trace of : number[] or tuple annotations at all. It becomes a completely ordinary JavaScript rest parameter. Types exist only to catch mistakes while you write the code.

Syntax

The general form of a rest parameter looks like this:

function fnName(fixedParam: string, ...restParam: number[]): void {
  // restParam is a number[] containing zero or more numbers
  console.log(fixedParam, restParam);
}
Part Meaning
... Marks the parameter as a rest parameter — it collects every remaining argument.
restParam The identifier bound to the collected arguments inside the function body.
: number[] An array type annotation — every collected argument must be a number.
: [string, ...unknown[]] A tuple type annotation — enforces a specific shape (at least one string, then anything) rather than a uniform array.

A rest parameter can only appear once per function, and it must be the final parameter — you cannot place a fixed parameter after it.

Examples

Example 1: A basic numeric rest parameter

function sum(...numbers: number[]): number {
  return numbers.reduce((total, n) => total + n, 0);
}

console.log(sum(1, 2, 3));
console.log(sum(10, 20, 30, 40));
console.log(sum());

Output:

6
100
0

Here numbers is typed as number[], so sum can be called with zero or more numeric arguments and the body can freely use array methods like reduce. If you tried sum(1, "2", 3), TypeScript would reject the call because "2" is not assignable to number.

Example 2: Combining a fixed parameter with a rest parameter

function formatMessage(prefix: string, ...parts: string[]): string {
  return prefix + ": " + parts.join(", ");
}

console.log(formatMessage("Errors", "missing name", "invalid email"));
console.log(formatMessage("Info"));

Output:

Errors: missing name, invalid email
Info: 

The first argument, prefix, is a required fixed parameter with its own type. Everything after it is collected into parts: string[]. Calling formatMessage("Info") is perfectly valid — a rest parameter is allowed to receive zero arguments, resulting in an empty array.

Example 3: A tuple-typed rest parameter for stricter calls

function logWithLevel(level: "info" | "warn" | "error", ...messages: [string, ...unknown[]]): void {
  console.log(`[${level.toUpperCase()}]`, ...messages);
}

logWithLevel("info", "Server started on port", 3000);
logWithLevel("error", "Failed to connect");

Output:

[INFO] Server started on port 3000
[ERROR] Failed to connect

Instead of typing messages as a plain unknown[], it’s typed as the tuple [string, ...unknown[]]. That forces every call to supply at least one message, and that first message must specifically be a string, while any further values can be anything. Calling logWithLevel("warn") with no message at all would fail to type-check, because the tuple demands a first string element.

Under the Hood: Step by Step

When the compiler checks a call like sum(1, 2, 3), it walks through this process:

1. It matches each argument position against the declared parameters, filling fixed parameters first.

2. Once fixed parameters are exhausted, every remaining argument is checked against the rest parameter’s element type (or, for a tuple rest type, against the corresponding tuple position).

3. Inside the function body, the rest parameter behaves like a real array (or tuple) — you get full access to .length, .map, .reduce, destructuring, and spreading, unlike the old, array-like arguments object from plain JavaScript, which has no array methods and no type information at all.

4. At compile time, all annotations are stripped. Because this course targets es2020 and later, the emitted JavaScript rest parameter is untouched — it’s simply function sum(...numbers) { ... }. (Older targets like ES5 would instead transpile it into code that manually slices arguments, but the type-checking behavior you rely on while writing the code stays the same regardless of target.)

Common Mistakes

Mistake 1: Putting the rest parameter before another parameter

A rest parameter must be the last one declared. Placing anything after it is both a syntax problem and a type error:

function bad(...numbers: number[], label: string) {
  console.log(label, numbers);
}

tsc reports: “A rest parameter must be last in a parameter list.” Fix it by moving the fixed parameter first:

function fixed(label: string, ...numbers: number[]): void {
  console.log(label, numbers);
}

fixed("Scores", 90, 85, 100);

Mistake 2: Typing a rest parameter as a scalar instead of an array

It’s easy to forget that a rest parameter collects multiple values and accidentally annotate it as a single value’s type:

function total(...nums: number) {
  return nums.length;
}

tsc reports: “A rest parameter must be of an array type.” The fix is to use the array type, since the parameter always holds a collection of values, not one value:

function total(...nums: number[]): number {
  return nums.length;
}

console.log(total(1, 2, 3, 4));

Best Practices

  • Always annotate rest parameters explicitly — relying on inference risks an implicit any[] under --strict.
  • Prefer a specific element type (string[], number[]) over any[] so mismatched calls are caught early.
  • Reach for a tuple rest type like [string, ...unknown[]] when you need to guarantee at least one argument or enforce a mixed shape, not just “zero or more of the same type.”
  • Use rest parameters instead of the legacy arguments object — you get real array methods and real type checking.
  • Keep the rest parameter last; design your function signature with fixed, required parameters first and the variadic tail second.
  • When spreading an array into a rest parameter at a call site (fn(...values)), make sure values‘ element type matches what the rest parameter expects.

Practice Exercises

1. Write a function average that accepts any number of number arguments via a rest parameter and returns their arithmetic mean, returning 0 when called with no arguments.

2. Write a function joinWords that takes a required separator: string parameter followed by a rest parameter of string values, and returns them joined by the separator. Calling joinWords("-", "a", "b", "c") should produce "a-b-c".

3. Write a function assertAtLeastOne typed with a tuple rest parameter [number, ...number[]] that logs the first number and the count of the remaining numbers. Try calling it with zero arguments and observe the type error tsc reports.

Summary

  • A rest parameter (...name) collects any number of remaining arguments into one variable.
  • In TypeScript, rest parameters must be typed as an array type (like number[]) or a tuple type, and must always come last in the parameter list.
  • Tuple rest types (e.g. [string, ...unknown[]]) let you require specific leading arguments while still allowing a variadic tail.
  • Every argument passed into the rest slot is checked against its declared element type at compile time.
  • Types are fully erased at runtime — the compiled JavaScript is an ordinary rest parameter with no type information.
  • Prefer rest parameters over the old arguments object for better type safety and full array method support.