Spread and REST operators in JavaScript: differences and examples
Spread (...) and rest (...) share the same syntax but do opposite things. Spread unpacks an iterable into separate elements; rest collects separate elements into one array.
Theory
TL;DR
- Spread is like dumping a bag of ingredients onto the counter: everything comes out separately
- Rest is like scooping leftovers back into the bag: everything goes in together
- Main difference: spread lives on the right side (or inside a function call), rest lives on the left (in params or destructuring)
- Copying, merging, or passing data? Use spread. Capturing extra args or remaining props? Use rest
- Rest always returns an array, even when no extra elements are passed (returns
[])
Quick Example
// Spread: unpacks array into separate elements
const nums = [1, 2, 3];
const more = [...nums, 4, 5]; // [1, 2, 3, 4, 5]
// Spread in a function call
function add(a, b, c) { return a + b + c; }
add(...nums); // 6
// Rest: collects remaining args into an array
function sum(first, ...rest) {
return first + rest.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10, rest = [2, 3, 4]Context decides which one you get. Same three dots, different roles.
Key Difference
Spread appears on the right side of assignments or inside function calls. It breaks an iterable apart and gives you individual values. Rest appears on the left in function parameters or destructuring patterns. It gathers whatever is left over and wraps it in an array. One expands, the other compresses.
When to Use
- Copy an array without mutation:
[...arr] - Merge objects:
{ ...obj1, ...obj2 } - Pass an array as separate arguments to a function:
func(...arr) - Capture a variable number of function arguments:
function fn(a, ...rest) {} - Destructure with remaining properties:
const { x, ...rest } = obj
Comparison Table
| Feature | Spread (...) | Rest (...) |
|---|---|---|
| Position | Right side (assignments, calls) | Left side (params, destructuring) |
| Effect | Expands into individual elements | Collects remainders into array |
| In functions | func(...arr) passes separate args | func(a, ...rest) gathers extra args |
| Arrays | [...arr1, ...arr2] merges | const [first, ...rest] = arr splits |
| Objects (ES2018+) | { ...obj1, ...obj2 } merges | const { a, ...rest } = obj extracts |
| When to use | Copy, merge, pass elements | Variable args, remaining props |
How It Works
JavaScript engines treat ... as syntax sugar: spread iterates the source via Symbol.iterator (for arrays) or Object.keys (for objects), yielding values one by one. Rest scans the parameter list at call time and packages trailing arguments into a new array. Both create shallow copies, so nested objects share references with the original.
In React codebases, spread is everywhere. The pattern { ...state, updatedField: newValue } is how reducers stay immutable without pulling in extra libraries.
Common Mistakes
Passing an array to a rest function without spreading it first:
function add(...nums) { return nums.reduce((a, b) => a + b); }
add([1, 2, 3]); // [1,2,3] becomes the first arg, returns NaN
add(...[1, 2, 3]); // correct: 6Assuming spread does a deep copy:
const a = { nested: { x: 1 } };
const b = { ...a };
a.nested.x = 99;
console.log(b.nested.x); // 99 - nested object is shared!
// Fix: spread the nested object too
const c = { ...a, nested: { ...a.nested } };Spreading a non-iterable:
[...42]; // TypeError: 42 is not iterable
[...'abc']; // works: ['a', 'b', 'c'] - strings are iterable
[...new Set([1, 2, 2])]; // works: [1, 2] - Set is iterableRest must be the last parameter:
function fn(a, ...rest, b) {} // SyntaxError
function fn(a, ...rest) {} // correctReal-World Usage
- React:
<Button {...buttonProps} />spreads props onto JSX elements - Redux reducers:
{ ...state, count: state.count + 1 }for immutable updates - Express middleware:
{ ...req.query, filter: 'active' }adds query params - Deduplication with Set:
[...new Set([1, 2, 2, 3])]returns[1, 2, 3]
Follow-up Questions
Q: What does const a = [1]; const b = [...a]; a.push(2); console.log(b); output?
A: [1]. Spread creates a new array, so b is independent of a.
Q: What did developers use before object spread (ES2018)?
A: Object.assign({}, obj1, obj2). Babel still transpiles object spread to Object.assign for older build targets, so the output is identical.
Q: What does rest return when no extra arguments are passed?
A: An empty array []. This is cleaner than the old arguments object, which was array-like but not a real array.
Q: Can you spread a Set or Map?
A: Yes, both are iterable. [...new Set([1, 2, 2])] gives [1, 2]. Useful for quick deduplication.
Q: Why use rest parameters instead of the arguments object?
A: arguments is array-like but not a real array, so .map() and .reduce() don't work on it directly. Rest gives you an actual array. Arrow functions also have no arguments of their own at all.
Examples
Merging User Settings
const defaults = { theme: 'light', fontSize: 14, showSidebar: true };
const userPrefs = { fontSize: 18, language: 'en' };
// Later keys override earlier ones
const settings = { ...defaults, ...userPrefs };
// { theme: 'light', fontSize: 18, showSidebar: true, language: 'en' }Order matters. ...userPrefs comes second, so its fontSize overwrites the default. This pattern is standard in config merging and theme overrides.
Variable Argument Logger
function log(level, ...messages) {
const prefix = `[${level.toUpperCase()}]`;
console.log(prefix, ...messages); // spread messages back out
}
log('info', 'User logged in', 'userId:', 42);
// [INFO] User logged in userId: 42Rest collects messages into an array. Then spread passes them back as separate arguments to console.log. Rest in, spread out.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.