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: scatterSpread 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]); // 5A 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 booleanTakeaways
...namein a parameter list is a rest parameter — it turns "any number of arguments" into a real array you canfilter,map, andjoin....valuein 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
argumentsobject, and they make small utilities likedocsPathclean 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.
