Spread and rest operator
The ... operator is one syntax with two roles: spread unpacks an iterable or an object's properties into separate elements, while rest gathers the remaining elements into a single array or object. The role is decided not by the operator itself but by where it stands: in a literal or a function call it is spread, in function parameters and in destructuring it is rest.
Theory
TL;DR
...in a literal or a call expands values:[...arr],{...obj},sum(...nums)....in destructuring or in a parameter list gathers the remainder:[a, ...rest],function fn(...args).- Spread makes a shallow copy: nested objects and arrays are copied by reference.
- Spread over objects merges properties, and a later key overwrites an earlier one.
- It is the modern replacement for
concat,Object.assignandArray.prototype.slice.call(arguments).
Quick example
const numbers = [1, 2, 3];
const moreNumbers = [0, ...numbers, 4, 5]; // spread: the array is unpacked
console.log(moreNumbers); // [0, 1, 2, 3, 4, 5]
const [first, ...rest] = moreNumbers; // rest: the remainder is gathered
console.log(first); // 0
console.log(rest); // [1, 2, 3, 4, 5]Spread: unpacking arrays
Spread is used to unpack (expand) an array or an object into separate elements.
const numbers = [1, 2, 3];
const moreNumbers = [0, ...numbers, 4, 5];
console.log(moreNumbers); // [0, 1, 2, 3, 4, 5]Here ...numbers expanded the array [1, 2, 3] inside a new array.
Copying arrays:
const arr1 = [1, 2, 3];
const arr2 = [...arr1];
console.log(arr2); // [1, 2, 3]
console.log(arr1 === arr2); // false (a new array)This is a shallow copy: only the outer array is new.
Merging arrays:
const a = [1, 2];
const b = [3, 4];
const c = [...a, ...b];
console.log(c); // [1, 2, 3, 4]Spread in function calls
In a call, spread turns an array into a list of separate arguments.
function sum(a, b, c) {
return a + b + c;
}
const nums = [1, 2, 3];
console.log(sum(...nums)); // 6
...numsturns[1, 2, 3]intosum(1, 2, 3).
Spread with objects
The same syntax copies own enumerable properties from one object into another.
const user = { name: 'Alex', age: 25 };
const newUser = { ...user, city: 'Paris' };
console.log(newUser); // { name: 'Alex', age: 25, city: 'Paris' }When keys collide, the later value overwrites the earlier one:
const merged = { a: 1, ...{ a: 2, b: 3 } };
console.log(merged); // { a: 2, b: 3 }Rest: gathering the remainder
Rest works the other way round: it does not unpack, it gathers the elements that are left into an array or an object.
In arrays:
const [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 1
console.log(rest); // [2, 3, 4]In objects:
const user = { name: 'Alex', age: 25, city: 'Paris' };
const { name, ...info } = user;
console.log(name); // Alex
console.log(info); // { age: 25, city: 'Paris' }In functions:
function sum(...numbers) {
return numbers.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10...numbers gathers every argument into a real array, unlike the array-like arguments.
Spread vs rest and what it replaced
| Feature | Spread | Rest |
|---|---|---|
| What it does | Unpacks | Gathers |
| Where it is used | In a function call, when building an array or object | In a function declaration or in destructuring |
| Example | [...arr], {...obj}, fn(...args) | function fn(...args), [a, ...rest] |
Before and after:
| Before ES6 | With ES6 |
|---|---|
arr1.concat(arr2) | [...arr1, ...arr2] |
Object.assign({}, obj1, obj2) | {...obj1, ...obj2} |
Array.prototype.slice.call(arguments) | (...args) => {} |
Summary of the behaviour:
| Context | Behaviour of ... | Example |
|---|---|---|
| Spread (unpacking) | Expands an array or object | [...arr], {...obj}, sum(...nums) |
| Rest (remainder) | Gathers the remaining elements | [a, ...rest], function fn(...args) |
Common mistakes
-
Treating spread as a deep copy. Spread and rest are shallow: nested objects and arrays are copied by reference, so changing a nested field in the copy is visible in the original.
javascriptconst obj = { nested: { a: 1 } }; const copy = { ...obj }; copy.nested.a = 2; console.log(obj.nested.a); // 2, not 1A deep copy needs
structuredClone(obj)or a manual walk of the structure. -
Using
...outside a destructuring context.const ...rest = arr;is a syntax error; the correct form isconst [a, ...rest] = arr;. -
Putting rest anywhere but last.
function fn(...args, last) {}does not parse: a rest parameter must be the final one, because it swallows everything that is left. -
Forgetting key order in object spread.
{ ...defaults, ...options }and{ ...options, ...defaults }give different results, because the rightmost object wins. -
Confusing
...argswitharguments.argumentsis an array-like object that does not exist in arrow functions; a rest parameter always gives a real array withmap,filterandreduce.
Short Answer
Interview readyA concise answer to help you respond confidently on this topic during an interview.