JavaScript Spread & Rest
The ... token does two opposite-looking jobs in JavaScript. As spread it unpacks an iterable or object into another expression; as rest it gathers multiple things into one. Same three dots, different position. Together they handle a huge slice of everyday array, object, and function work — copying, merging, variable arity, and partial application.
The Two Faces of ...
| Position | Role | Example | Meaning |
|---|---|---|---|
| Inside an array/object literal or call args | spread | [...a] f(...args) { ...obj } |
unpack |
| Inside a destructuring pattern or parameter list | rest | [a, ...b] ({a, ...b}) (a, ...b) => |
gather |
// Spread — unpacks
const nums = [1, 2, 3];
console.log(...nums); // 1 2 3 (three arguments to console.log)
// Rest — gathers
function sum(...args) { return args.reduce((a, b) => a + b, 0); }
sum(1, 2, 3); // 6
Spread for Arrays
Copy
const a = [1, 2, 3];
const b = [...a];
a === b; // false — different arrays
a[0] === b[0]; // true (shallow copy)
Shallow — nested objects are still shared:
const a = [{ x: 1 }];
const b = [...a];
b[0].x = 99;
console.log(a[0].x); // 99 — same nested object
Concat
const a = [1, 2];
const b = [3, 4];
[...a, ...b]; // [1, 2, 3, 4]
[...a, 99, ...b]; // [1, 2, 99, 3, 4]
[0, ...a, ...b, 99]; // [0, 1, 2, 3, 4, 99]
More readable than a.concat(b).
From Iterable to Array
... works on any iterable — strings, Set, Map, NodeList, generator, arguments…
[..."abc"]; // ["a", "b", "c"]
[...new Set([1, 2, 2, 3])]; // [1, 2, 3] — dedupe trick!
[...document.querySelectorAll("li")];
// Equivalent: Array.from(x)
Array.from("abc");
A famous one-liner: dedupe an array.
const unique = [...new Set([1, 2, 2, 3, 3, 3])];
// [1, 2, 3]
Spread Into Function Calls
Replaces the old apply trick:
const nums = [5, 1, 9, 3];
Math.max(...nums); // 9
Math.max.apply(null, nums); // 9, old form
console.log(...["a", "b"]); // "a" "b"
Useful for forwarding arguments:
function logCall(fn, ...args) {
console.log("calling with", args);
return fn(...args);
}
Spread for Objects
const a = { x: 1, y: 2 };
const b = { y: 99, z: 3 };
// Copy
const copy = { ...a };
// Merge — later keys win
const merged = { ...a, ...b };
// { x: 1, y: 99, z: 3 }
// Add/override
const updated = { ...a, y: 100, w: 4 };
// { x: 1, y: 100, w: 4 }
| Object spread | Object.assign({}, ...) |
|
|---|---|---|
| Readability | high | medium |
| Triggers setters on target | no | yes |
| Recommended | ✅ default | only when you need setters |
Warning: Object spread is shallow — nested objects are shared. To deep clone use structuredClone(obj).
const original = { profile: { age: 30 } };
const copy = { ...original };
copy.profile.age = 99;
console.log(original.profile.age); // 99 — same nested object!
Spread + Default for Immutable Updates
The Redux / React pattern for “update one key”:
const state = { user: "Ada", count: 0, active: true };
const next = { ...state, count: state.count + 1 };
// state is unchanged; next is { user: "Ada", count: 1, active: true }
Nested updates need explicit nested spread:
const state = { user: { name: "Ada", age: 30 } };
const next = {
...state,
user: { ...state.user, age: 31 },
};
Rest in Function Parameters
Collect any number of trailing arguments into an array:
function tag(label, ...items) {
return `${label}: ${items.join(", ")}`;
}
tag("fruits", "apple", "banana"); // "fruits: apple, banana"
tag("empty"); // "empty: "
tag("one", "two", "three", "four"); // "one: two, three, four"
Rules:
- ✅ Rest must be last in the parameter list
- ✅ Rest is always an array (never
null/undefined) - ✅ Use instead of the legacy
argumentsobject (which doesn’t exist on arrows anyway)
function bad(...args, last) {} // ❌ SyntaxError
function ok(first, ...args) {} // ✅
Rest in Destructuring
Already met in chapter 33 — gather the leftovers.
// Array
const [head, ...tail] = [1, 2, 3, 4];
// head = 1, tail = [2, 3, 4]
// Object
const { name, ...rest } = { name: "Ada", age: 37, role: "admin" };
// name = "Ada", rest = { age: 37, role: "admin" }
A common pattern: “everything except these fields”
function publicView(user) {
const { password, internalId, ...safe } = user;
return safe;
}
publicView({ id: 1, name: "Ada", password: "secret", internalId: "x" });
// { id: 1, name: "Ada" }
Spread + Rest Together — Forwarding
function withTiming(fn) {
return (...args) => { // rest: gather
const start = performance.now();
const result = fn(...args); // spread: forward
console.log(`${fn.name} took ${performance.now() - start} ms`);
return result;
};
}
const slowSum = withTiming((a, b) => {
for (let i = 0; i < 1e6; i++) {} // pretend it's slow
return a + b;
});
slowSum(2, 3); // logs the timing → 5
...args gathers, fn(...args) spreads. The pair is the canonical higher-order wrapper.
Cloning vs Referencing — Cheatsheet
// Shallow clone
const arrCopy = [...arr];
const objCopy = { ...obj };
// Deep clone (modern, ES2022+)
const deep = structuredClone(obj);
// Deep clone (JSON-safe only — loses functions, dates → strings)
const deepJson = JSON.parse(JSON.stringify(obj));
// In-place identity check
arr === arr; // true
arr === [...arr]; // false
A Realistic Example — Config Builder
const DEFAULTS = Object.freeze({
timeout: 5000,
retries: 3,
headers: { "Content-Type": "application/json" },
});
function request(url, opts = {}) {
// Merge top-level + deep-merge headers
const config = {
...DEFAULTS,
...opts,
headers: { ...DEFAULTS.headers, ...(opts.headers ?? {}) },
};
console.log("→ fetching", url, "with", config);
// (actual fetch here)
}
request("/api", { retries: 5, headers: { "X-Trace": "abc" } });
// → fetching /api with
// { timeout: 5000, retries: 5,
// headers: { Content-Type: ..., X-Trace: "abc" } }
Note the explicit nested merge for headers — a flat spread alone would have replaced the whole object.
Common Mistakes
// ❌ Trying to spread null / undefined as object → silently treated as {}
const merged = { ...null }; // {} no error
const arr = [...null]; // ❌ TypeError — null is not iterable
// ❌ Spread inside a non-array/object context
const x = ...arr; // ❌ SyntaxError
// ❌ Rest in the middle
function f(a, ...mid, b) {} // ❌ SyntaxError — rest must be last
// ❌ Expecting deep clone from spread
const a = { nested: { x: 1 } };
const b = { ...a };
b.nested.x = 99;
// a.nested.x is also 99
// ❌ Performance — spreading huge arrays in tight loops
let acc = [];
for (const chunk of bigChunks) {
acc = [...acc, ...chunk]; // O(n²) — copies the whole acc each time
}
// ✅
const acc = [];
for (const chunk of bigChunks) acc.push(...chunk);
Best Practices Checklist
- ✅ Use spread to clone (
[...arr],{ ...obj }) and merge — readable and standard - ✅ Use rest in parameters instead of
arguments - ✅ Use rest in destructuring to express “everything except these”
- ✅ Use
structuredClonefor deep copies - ✅ Cap object iterables — spread on iterables is O(n)
- ❌ Don’t assume spread is deep — it’s shallow
- ❌ Don’t put rest in the middle of a parameter list
- ❌ Don’t repeatedly rebuild large arrays with spread — push instead
What’s Next?
You’ve mastered the building blocks of JS data. Time to serialise and exchange it — the language’s universal data format and its built-in JSON object.