Chapter 2.3☕ 12 min read

Rest Parameters

Pack the rest into an array — no more, no less.

01The RTC Bus Stop

Sometimes you genuinely don't know how many arguments a function will receive when you call it. Maybe the caller passes 2, maybe 5, maybe 20. You can't exactly declare 20 optional parameters, can you? That would be absurd. This is exactly the problem rest parameters solve — they let you collect "the rest" of the arguments into a single array.

Think of the RTC bus at Secunderabad bus stop. The bus has a fixed route and a driver — those are your required parameters. But it keeps picking up passengers along the way. Could be 10 passengers, could be 50, could be standing-room-only packed like sardines. The driver doesn't know exactly how many passengers will board at each stop, but the bus has space for "the rest." The first few stops might have fixed riders (your required parameters), but everyone after that? They all pile in — and that's your rest parameter, an array of however many showed up.

Or think of a shawarma roll from that famous spot near Banjara Hills. The wrap itself is fixed — that's your required parameter. But the fillings? "Bhai, aur chicken daal, aur onion daal, aur sauce daal, aur mayo daal..." You don't decide the number of extras in advance. You just keep adding. The kitchen doesn't pre-count how many extras you'll ask for — they just pack whatever you request into the roll.

Rest parameters are exactly that concept in code. When you declare a function with a rest parameter, you're telling TypeScript: "I don't know how many arguments will be passed for this part, but pack whatever's left into an array for me." The array is properly typed, properly scoped, and gives you all the array methods you love — .reduce(), .map(), .filter(), .join(), everything. It's clean, it's predictable, and it replaces the old, untyped arguments object that JavaScript developers used to struggle with.

02Rest Parameter Syntax

The syntax for rest parameters uses the ... (spread/rest operator) before the last parameter name. Here's the simplest form:

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

sum(1, 2, 3);    // nums = [1, 2, 3] → returns 6
sum(10);          // nums = [10] → returns 10
sum();            // nums = [] → returns 0

Notice three things: (1) the ... operator before the parameter name, (2) the type is number[] — it must be an array type, and (3) every argument you pass after the fixed parameters gets collected into this array. When you call sum(1, 2, 3), inside the function nums is literally the array [1, 2, 3]. You can use any array method on it.

Rest parameters work beautifully alongside regular fixed parameters. The rule is simple: rest must be the last parameter. Everything before it is a normal required (or optional) parameter, and everything after the last fixed parameter gets swept into the rest array:

function log(level: string, ...messages: string[]): void {
  console.log(`[${level}]`, messages.join(", "));
}

log("error", "disk full", "retrying");
// level = "error", messages = ["disk full", "retrying"]

log("info", "server started");
// level = "info", messages = ["server started"]

TypeScript enforces the element type strictly. If you declare ...nums: number[], then every argument collected must be a number. Calling sum(1, "two", 3) is a compile-time error because "two" is not a number. This is the whole point of TypeScript — it catches these mistakes before your code runs.

Arrow functions support rest parameters too:

const join = (...parts: string[]): string => parts.join("-");

join("dev", "in", "hyderabad"); // "dev-in-hyderabad"
join("hello");                   // "hello"

You can have multiple fixed parameters before the rest parameter. The rest parameter simply collects all remaining arguments:

function createProfile(
  name: string,
  age: number,
  ...hobbies: string[]
): string {
  return `${name}, ${age} — likes: ${hobbies.join(", ")}`;
}

createProfile("Rahul", 25, "coding", "biryani", "cricket");
// name = "Rahul", age = 25, hobbies = ["coding", "biryani", "cricket"]

The first two arguments are fixed — they map to name and age. Everything after that — no matter how many — goes into hobbies. This pattern is incredibly common in real-world code: formatting functions, logging utilities, event handlers, and SQL query builders all use rest parameters to accept a flexible number of trailing arguments.

03Rest with Tuple Types

Basic rest parameters with ...args: type[] are great, but sometimes you need more precision. TypeScript lets you type rest parameters using tuple types, which give you exact control over the number and types of arguments. This is where rest parameters get really powerful.

The simplest tuple rest looks like this:

function send(...args: [string, number]): void {
  const [message, priority] = args;
  console.log(`Msg: ${message}, Priority: ${priority}`);
}

send("Hello", 1);   // ✅ Exactly 2 args — string then number
send("Hi");          // ❌ Error: Expected 2 arguments
send("Hi", 1, 2);   // ❌ Error: Expected 2 arguments

With ...args: [string, number], the function must receive exactly two arguments — the first a string, the second a number. They get collected into a tuple, not a loose array. This is incredibly useful for type-safe wrappers, decorators, and callback signatures where the exact shape matters.

Now here's where it gets interesting — the "leading element + rest" pattern. You can mix a fixed first element with a rest tail inside the tuple:

function query(...args: [string, ...(string | number)[]]): void {
  const [sql, ...params] = args;
  console.log("SQL:", sql, "Params:", params);
}

query("SELECT * FROM users");                     // sql = "SELECT * FROM users", params = []
query("SELECT * FROM users WHERE id = ?", 42);    // sql = "SELECT * FROM users WHERE id = ?", params = [42]
query("INSERT INTO logs VALUES (?, ?, ?)", 1, "login", "success");

This pattern — [string, ...(string | number)[]] — means: the first argument must be a string (the SQL query), and then any number of additional arguments that are either strings or numbers (the parameter values). You get type safety on the first arg AND flexibility on the rest. This is exactly how real database query functions work.

Another common pattern — requiring at least one argument:

function max(...nums: [number, ...number[]]): number {
  return Math.max(...nums);
}

max(5);           // ✅ At least 1 number required
max(3, 7, 2);     // ✅
max();            // ❌ Error: Expected at least 1 argument

With ...nums: number[], calling max() with zero arguments is valid (the array is just empty). But with ...nums: [number, ...number[]], you guarantee at least one number. The first slot of the tuple is required, and the rest is optional. This prevents the bug where Math.max() returns -Infinity when called with no arguments — a classic JavaScript footgun.

Why use rest instead of the old arguments keyword? Three reasons: (1) arguments is not a real array — no .map(), .reduce(), .filter() without conversion. (2) arguments is untyped — TypeScript can't enforce what goes in. (3) arguments doesn't work in arrow functions at all. Rest parameters solve all three problems — they're real arrays, they're typed, and they work everywhere. Always prefer rest over arguments.

04Rest Parameter Traps

Rest parameters are straightforward once you know the rules, but there are several traps that catch beginners and even experienced developers off guard. Let's go through each one carefully.

Trap 1: Rest parameter must be LAST. This is the most common mistake. You cannot put any parameter after a rest parameter:

// ❌ ERROR — rest parameter must be last
function f(...nums: number[], last: string): void { }

// ✅ CORRECT — rest comes after everything else
function f(last: string, ...nums: number[]): void { }

Why? Because rest parameters collect all remaining arguments. If TypeScript allowed a parameter after rest, it would have no way to know where the rest ends and the next parameter begins. The compiler will give you a clear error: A rest parameter must be last in a parameter list. There are no exceptions to this rule.

Trap 2: Rest parameter must be an array type. You can't just write ...rest: string — that's invalid:

// ❌ ERROR — rest must be array or tuple type
function f(...rest: string): void { }

// ✅ CORRECT — use array type
function f(...rest: string[]): void { }

// ✅ CORRECT — use tuple type
function f(...rest: [string, number]): void { }

The rest parameter collects multiple values, so it must be something that can hold multiple values — either an array type like string[] or a tuple type like [string, number]. A plain string makes no sense because it can only hold one value.

Trap 3: Only ONE rest parameter per function. You cannot have two rest parameters:

// ❌ ERROR — only one rest parameter allowed
function f(...a: number[], ...b: string[]): void { }

// ✅ If you need mixed types, use a union in a single rest
function f(...items: (number | string)[]): void { }

Again, this is logically impossible. If you had two rest parameters, TypeScript would have no way to know where ...a ends and ...b begins. One rest per function — that's the rule.

Trap 4: Spreading an array into rest arguments. When you have an existing array and want to pass it as rest arguments, you use the spread operator in reverse:

const nums = [1, 2, 3];
sum(...nums); // This works at runtime

But TypeScript might complain that nums is typed as number[], which is a mutable array of any length, when the function might expect a specific tuple shape. The fix is to use as const:

const nums = [1, 2, 3] as const;
// Now nums is readonly [1, 2, 3] — a precise tuple
sum(...nums); // ✅ TypeScript is happy

Trap 5: Empty rest calls are valid. Calling sum() with zero arguments is perfectly valid when the signature is ...nums: number[], because the array can be empty. This can lead to unexpected behavior — like Math.max() returning -Infinity when called with no args. If you need at least one argument, use the tuple pattern: ...nums: [number, ...number[]]. This enforces that the first element is required while the rest are optional.

05Rest Parameters Cheatsheet

Let's consolidate everything about rest parameters into a quick-reference cheatsheet you can come back to anytime.

Basic Syntax:

function f(...args: type[]): returnType {
  // args is an array of 'type'
}

With Other Parameters:

function f(required: type, ...rest: type[]): returnType {
  // first arg → required, everything else → rest array
}

Tuple Rest (exact count):

function f(...args: [string, number]): void {
  // Must be called with exactly (string, number)
}

Leading + Rest (at least one):

function f(...args: [string, ...number[]]): void {
  // First arg must be string, then any number of numbers
}

The Four Golden Rules:

  • Rule 1: Rest must be the last parameter. No parameters after it. Ever.
  • Rule 2: Only one rest parameter per function. No exceptions.
  • Rule 3: Rest must be an array type (type[]) or a tuple type ([A, B]). No plain types.
  • Rule 4: With ...args: type[], calling with zero rest arguments is valid (empty array). Use tuple rest like [type, ...type[]] if you need at least one.

Quick Comparison:

  • ...args: number[] → any number of numbers, including zero
  • ...args: [number, number] → exactly two numbers
  • ...args: [number, ...number[]] → at least one number
  • ...args: [string, ...number[]] → first string, then any numbers

Rest vs arguments:

  • arguments is not a real array — rest IS a real array
  • arguments is untyped — rest is fully typed
  • arguments doesn't work in arrow functions — rest works everywhere
  • Always prefer rest over arguments

The golden rule: "When you don't know how many — use rest. But remember, rest means 'the remaining ones packed into an array,' not 'any type whatsoever.' Type your rest properly!"

Key Points — Rest Parameters

  • Rest parameters collect remaining arguments into a typed array using `...param: type[]` syntax.
  • A rest parameter MUST be the last parameter in the function signature.
  • Only ONE rest parameter is allowed per function.
  • Rest must be an array type (like `string[]`) or a tuple type (like `[string, number]`).
  • Use tuple rest like `...args: [string, ...number[]]` to require at least one argument.
Course Search
Search across all chapters & stages
📖

Search the course

Type any topic — branching, stash, rebase, hooks — and jump straight to that chapter.

merge branchesgit stashundo commitrebase