JavaScript Spread vs Rest: Same Syntax, Different Jobs
Master JavaScript's ... operator. Learn when spread expands values and when rest collects them, with examples, gotchas, and deep clone alternatives.
JavaScript Spread vs Rest: Same Syntax, Different Jobs
The three dots (...) in JavaScript wear two hats. Spread takes an iterable or object and expands it into individual pieces. Rest does the opposite: it collects loose pieces into a single array or object. The syntax is identical; the direction of data flow is what changes.
The rule of thumb
If ... appears on the right side of an assignment or inside a function call, array literal, or object literal, it is spreading. If it appears on the left side (a parameter list or destructuring pattern), it is resting.
Where each one lives
| Context | Operator | Direction |
|---|---|---|
Function call: fn(...args) | Spread | Expands into arguments |
Array literal: [...a, ...b] | Spread | Expands into elements |
Object literal: {...a, ...b} | Spread | Expands into properties |
Function parameter: function fn(...args) | Rest | Collects into array |
Array destructuring: const [a, ...rest] = arr | Rest | Collects remaining items |
Object destructuring: const {a, ...rest} = obj | Rest | Collects remaining keys |
Spread in action
// Expand array into function arguments
const nums = [3, 1, 4, 1, 5];
Math.max(...nums); // 5
// Merge arrays
const combined = [...nums, 9, 2, 6];
// Clone and extend an object
const user = { name: 'Ada', role: 'admin' };
const updated = { ...user, role: 'owner' }; // right wins
// { name: 'Ada', role: 'owner' }When object spread encounters duplicate keys, the rightmost value wins. This is how patch-style updates work in Redux reducers and React state setters.
Rest in action
// Collect any number of arguments
function sum(...values) {
return values.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10
// Split head from tail
const [first, ...rest] = ['a', 'b', 'c', 'd'];
// first = 'a', rest = ['b', 'c', 'd']
// Strip a field from an object
const { password, ...safeUser } = user;Rest has one hard rule: it must be the last parameter or destructured slot. Writing function fn(...args, last) is a syntax error.
The shallow copy gotcha
Spread creates a copy, but only one level deep. Nested objects and arrays are still shared by reference.
const original = { name: 'Ada', address: { city: 'London' } };
const copy = { ...original };
copy.address.city = 'Paris';
console.log(original.address.city); // 'Paris' - mutated!The top-level address reference was copied, not the object it points to. Both objects now share the same inner address.
Deep clone alternatives
Three options, from oldest to best:
// 1. JSON round-trip (legacy trick)
const deep1 = JSON.parse(JSON.stringify(original));
// Loses: functions, undefined, Date (becomes string),
// Map, Set, RegExp, cyclic references (throws)
// 2. structuredClone (modern, recommended)
const deep2 = structuredClone(original);
// Handles: Date, Map, Set, RegExp, TypedArrays, cycles
// Does not clone: functions, DOM nodes, class prototypes
// 3. Manual spread for known shapes
const deep3 = {
...original,
address: { ...original.address }
};structuredClone ships in every modern browser and in Node 17+. Reach for it first. Use the manual spread pattern when you only need to isolate one or two nested branches and want to keep the copy cheap.
Quick mental model
- Spread unpacks a container so its contents can be reused elsewhere.
- Rest packs loose values into a container so they can be handled as a group.
- Same three dots, opposite directions, always shallow at the boundary they operate on.