Rest Parameters and Spread: The Two Faces of ... in JavaScript & TypeScript

I recently ran into this small helper while refactoring a router in our codebase:

export function docsPath(...segments: string[]): string {
  const path = segments.filter(Boolean).join("/");
  return path ? `${DOCS_BASE}/${path}` : DOCS_BASE;
}

The part that made me pause was ...segments. I'd seen the ... operator a hundred times when copying objects ({ ...props }) or arrays ([...items]), but I'd never really thought about what it does inside a function's parameter list. It turns out ... wears two different hats depending on where you put it — and understanding the difference makes a lot of everyday code click into place.

The problem rest parameters solve

Say you want a function that joins URL segments into a path. How many segments will the caller pass? You don't know — sometimes one, sometimes three, sometimes none:

docsPath("guides");                 // "/docs/guides"
docsPath("guides", "my-slug");      // "/docs/guides/my-slug"
docsPath("customers", "acme", "x"); // "/docs/customers/acme/x"
docsPath();                         // "/docs"

Before rest parameters, you'd have to lean on the magic arguments object, which is clumsy and isn't a real array:

function docsPath() {
  // `arguments` is array-LIKE, but you can't call .filter or .join on it directly
  const segments = Array.prototype.slice.call(arguments);
  // ...
}

Rest parameters replace all of that with one tidy piece of syntax.

What ...segments actually means

When ... appears in a function's parameter list, it's a rest parameter. It says:

"Collect however many arguments the caller passes into a single real array named segments."

function docsPath(...segments: string[]) {
  // segments is a genuine Array<string>
}

docsPath("a", "b", "c"); // segments === ["a", "b", "c"]
docsPath();              // segments === []

Because segments is a real array, you get every array method for free — which is exactly what the helper uses:

const path = segments.filter(Boolean).join("/");

A few rules worth remembering:

  • A function can have only one rest parameter.

  • It must be last in the parameter list (it scoops up "the rest").

  • You can combine it with regular parameters:

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

logEvent("info", "user", "logged", "in");
// level === "info"
// messages === ["user", "logged", "in"]

Walking through the helper

Let's trace docsPath with DOCS_BASE = "/docs":

docsPath("guides", "my-slug");
// 1. segments = ["guides", "my-slug"]   ← rest parameter collects args
// 2. .filter(Boolean)  → ["guides", "my-slug"]   (drops "", undefined, null)
// 3. .join("/")        → "guides/my-slug"
// 4. path is truthy    → "/docs/guides/my-slug"

docsPath();
// 1. segments = []
// 2. .filter(Boolean) → []
// 3. .join("/")       → ""
// 4. path is falsy    → "/docs"   (just the base)

The .filter(Boolean) step is a nice defensive touch. It lets callers pass conditional or possibly-empty segments without producing ugly double slashes:

const slug = maybeUndefined();        // could be undefined or ""
docsPath("guides", slug);             // still "/docs/guides", never "/docs/guides/"

(Boolean as a callback removes every falsy value: "", undefined, null, 0, NaN, false.)

The other hat: spread

Here's the twist. The exact same ... token does the opposite job when it appears in a function call or an array/object literal. There it's the spread operator: it takes an existing iterable and expands it into individual elements.

const parts = ["customers", "acme"];

docsPath(...parts);
// equivalent to docsPath("customers", "acme")

So:

  • Rest (in a parameter list): gathers many values into one array.

  • Spread (in a call / literal): scatters one array into many values.

They're mirror images. The clearest way to see it is to use both together:

function docsPath(...segments: string[]) {   // rest: gather
  return "/docs/" + segments.join("/");
}

const parts = ["a", "b", "c"];
docsPath(...parts);                           // spread: scatter

Spread shows up in lots of other places too:

// Copy / merge arrays
const merged = [...arr1, ...arr2];

// Copy / merge objects
const updated = { ...user, name: "New Name" };

// Pass an array where individual args are expected
Math.max(...[3, 1, 4, 1, 5]); // 5

A quick mental model

If you only remember one thing, remember where the ... is:

LocationNameWhat it doesFunction parameter listRestCollects arguments into an arrayFunction callSpreadExpands an array into argumentsArray literal [...]SpreadInlines elements into a new arrayObject literal {...}SpreadInlines properties into a new object

Same syntax, opposite directions — gather vs. scatter.

TypeScript notes

In TypeScript, you type a rest parameter as an array, and every collected argument must match the element type:

function docsPath(...segments: string[]): string { /* ... */ }

docsPath("a", "b");   // ✅
docsPath("a", 42);    // ❌ Argument of type 'number' is not assignable to 'string'

You can even type heterogeneous rest parameters with tuple types, which is how typed event emitters and console.log-style signatures are built:

function emit(event: string, ...args: [id: number, ok: boolean]) {}

emit("done", 1, true);  // ✅
emit("done", 1);        // ❌ missing the boolean

Takeaways

  • ...name in a parameter list is a rest parameter — it turns "any number of arguments" into a real array you can filter, map, and join.

  • ...value in a call or literal is the spread operator — it expands an array/object back into individual pieces.

  • They're inverses: rest gathers, spread scatters.

  • Rest parameters are the modern, type-safe replacement for the old arguments object, and they make small utilities like docsPath clean and flexible.

A tiny piece of syntax, but once you see both faces of ..., a surprising amount of idiomatic JavaScript and TypeScript stops looking like magic.