Spread, Rest & Default Params
One syntax (…) two opposite jobs, plus default parameters — the trio everyone confuses, made clear with a mental model, diagram, and gotchas.
Three features people blur together: default parameters fill in missing arguments, and ... does two opposite things depending on where it sits.
The one idea that unlocks it 🎯#
... is the same token with opposite jobs, decided by position:
- Rest — on the left of
=/ in a parameter list / in destructuring → it collects many values into one array/object. - Spread — on the right / inside a call, array, or object literal → it expands one iterable into many values.
Rule of thumb: rest packs, spread unpacks. If
...is receiving, it's rest. If it's supplying, it's spread.
flowchart LR
subgraph S["spread … — EXPANDS (one → many)"]
SA["[1, 2, 3]"] --> s1["1"]
SA --> s2["2"]
SA --> s3["3"]
end
subgraph R["rest … — COLLECTS (many → one)"]
r1["1"] --> RA["…args → [1, 2, 3]"]
r2["2"] --> RA
r3["3"] --> RA
end
Rest ... |
Spread ... |
|
|---|---|---|
| Job | collects values into one | expands one into values |
| Lives in | param lists, destructuring (the target) | calls, array/object literals (the source) |
| Produces | a real array (or object) | individual elements |
| Position | must be last | anywhere |
| Example | function f(...args) |
f(...arr) |
Default parameters#
A parameter gets a fallback when the argument is undefined (only undefined — not null, 0, or "").
function greet(name = "Guest", greeting = "Hi") {
return `${greeting}, ${name}`;
}
greet(); // "Hi, Guest"
greet("Ada"); // "Hi, Ada"
greet(undefined, "Yo"); // "Yo, Guest" — undefined triggers the default
greet(null); // "Hi, null" ⚠️ null does NOT trigger it
Key facts 🎯:
- Evaluated at call time, left to right, so a later default can use an earlier param:
(a, b = a * 2) => {}. - Defaults can be any expression (function calls, objects):
(id = crypto.randomUUID()) => {}. - Combine with destructuring for clean "options objects" — the
= {}lets you omit the whole argument:
function connect({ host = "localhost", port = 5432, tls = false } = {}) {
return `${tls ? "tls" : "tcp"}://${host}:${port}`;
}
connect(); // uses all defaults (thanks to `= {}`)
connect({ port: 6432 }); // override just one
Rest — collect the leftovers#
// rest parameter — gathers remaining args into a real array
function sum(first, ...rest) {
return rest.reduce((a, b) => a + b, first);
}
sum(1, 2, 3, 4); // 10 (rest = [2, 3, 4])
// rest in destructuring
const [head, ...tail] = [1, 2, 3]; // head = 1, tail = [2, 3]
const { id, ...others } = { id: 1, a: 2, b: 3 }; // others = { a: 2, b: 3 }
⚠️ Rest must be last — function f(...args, last) {} is a SyntaxError.
🟢 Prefer ...args over the legacy arguments object: rest is a real array (has .map, .filter), arrow functions have it, and it excludes the params you already named.
Spread — expand into place#
// in function calls
Math.max(...[3, 1, 2]); // 3
const nums = [1, 2]; sum(...nums, 3);
// in array literals — clone, merge, insert
const clone = [...arr]; // shallow copy
const merged = [...a, ...b]; // concatenate
const inject = [0, ...mid, 99]; // insert in the middle
// in object literals — clone, merge, override
const patched = { ...user, active: true }; // later keys win 🎯
const withDefaults = { role: "user", ...input }; // input overrides defaults
// spread any iterable
[...new Set([1, 1, 2])]; // [1, 2] (dedupe)
[..."hi"]; // ["h", "i"]
⚠️ Spread is a shallow copy — nested objects/arrays are still shared by reference:
const a = { u: { name: "x" } };
const b = { ...a };
b.u.name = "y";
a.u.name; // "y" ⚠️ nested object was shared
// deep copy instead:
const deep = structuredClone(a);
All three together#
function createUser(name = "Anonymous", ...roles) { // default + rest
return { name, roles };
}
const base = ["reader"];
createUser("Ada", ...base, "admin"); // spread into the call
// → { name: "Ada", roles: ["reader", "admin"] }
Interview Q&A#
Q1. Same ... — how do you know if it's rest or spread?
Position decides: if ... is receiving values (parameter list, destructuring target) it's rest and packs them into one array/object; if it's supplying values (a call, array, or object literal) it's spread and unpacks one iterable into many.
Q2. Does a default parameter trigger for null? For 0?
Only for undefined. null, 0, and "" are all passed through as-is — e.g. greet(null) yields "Hi, null", not the default.
Q3. Why prefer a rest param over arguments?
...args is a real array (has .map/.filter/.reduce), it's available inside arrow functions (where arguments isn't), and it excludes the parameters you already named.
Q4. Is object/array spread a deep copy? How do you deep-copy?
No — spread is a shallow copy; nested objects/arrays remain shared by reference. Deep-copy with structuredClone(obj) (or a library / JSON.parse(JSON.stringify(...)) with its caveats).
Q5. Why must the rest parameter come last?
Rest collects all remaining arguments, so anything after it would be ambiguous — function f(...args, last) {} is a SyntaxError.