Destructuring, Spread & Rest
Destructuring, spread (...), and rest (...) are three closely related ES2015+ features that share syntax but solve opposite problems: destructuring pulls values out of arrays and objects, spread expands an iterable or object into a new one, and rest collects leftover values into a fresh array or object. Together they replace a huge amount of boilerplate — manual indexing, Object.assign, apply, and arguments juggling. This page brings them all into one practical reference with idioms you’ll use daily.
Array destructuring
Wrap variable names in [] on the left side; each binds to the element at the same position. Names without a matching element become undefined, and any binding can declare a default with = that applies only when the matched value is undefined.
const [first, second = "n/a", third = "n/a"] = ["a", "b"];
console.log(first, second, third);
// Skip positions with holes, capture the tail with rest
const [, , ...rest] = [10, 20, 30, 40];
console.log(rest);
Output:
a b n/a
[ 30, 40 ]
Because destructuring is positional, it works on any iterable — strings, Sets, and Map entries. A classic idiom is swapping without a temp variable:
let x = 1;
let y = 2;
[x, y] = [y, x];
console.log(x, y);
Output:
2 1
Object destructuring
Object patterns match by key name, not position, so order is irrelevant. You can rename bindings with key: alias, supply defaults, and combine the two. Nested patterns reach into deep structures in a single statement.
const user = {
id: 7,
name: "Ada",
address: { city: "London" },
};
const { name, role = "guest", address: { city } } = user;
console.log(name, role, city);
Output:
Ada guest London
A rest pattern in an object collects the remaining own enumerable properties into a new object — perfect for “everything except” operations.
const { id, ...withoutId } = user;
console.log(withoutId);
Output:
{ name: 'Ada', address: { city: 'London' } }
When destructuring into existing variables (not a
const/letdeclaration), wrap the whole statement in parentheses:({ a, b } = obj);. A leading{is otherwise parsed as a block.
Spread in arrays and objects
The spread operator expands the contents of an iterable or object into a new literal. It’s the idiomatic way to copy and merge without mutating the source, which matters for predictable state updates in frameworks like React.
const base = [1, 2];
const more = [0, ...base, 3]; // insert anywhere
const copy = [...base]; // shallow clone
const defaults = { theme: "dark", size: "m" };
const merged = { ...defaults, size: "l" }; // later keys win
console.log(more);
console.log(merged);
Output:
[ 0, 1, 2, 3 ]
{ theme: 'dark', size: 'l' }
Object spread (ES2018) only copies own enumerable properties, and like array spread it’s a shallow copy — nested objects are shared by reference. Spread also converts any iterable into an array, replacing Array.from for simple cases:
console.log([...new Set([1, 1, 2, 3])]); // dedupe -> [1, 2, 3]
console.log(Math.max(...[4, 9, 2])); // spread as arguments -> 9
Rest parameters
Rest gathers an arbitrary number of trailing arguments into a real array — unlike the legacy arguments object, it’s a genuine Array with map, filter, and friends. It must be the last parameter.
function sum(label, ...nums) {
const total = nums.reduce((acc, n) => acc + n, 0);
return `${label}: ${total}`;
}
console.log(sum("scores", 10, 20, 30));
Output:
scores: 60
You can combine parameter destructuring with rest to build clean configurable functions:
function createButton({ label, ...attrs } = {}) {
return { type: "button", label, attrs };
}
console.log(createButton({ label: "OK", disabled: true, id: "ok" }));
Output:
{ type: 'button', label: 'OK', attrs: { disabled: true, id: 'ok' } }
Spread vs rest at a glance
Both use ..., but context decides their meaning: on the right of = or inside a call/literal it spreads (expands); on the left in a pattern or a parameter list it rests (collects).
| Syntax | Role | Result |
|---|---|---|
[...arr], {...obj} | Spread | Expands into a new array/object |
f(...args) | Spread | Passes elements as separate arguments |
const [a, ...r] = arr | Rest | Collects remaining elements into an array |
const { a, ...r } = obj | Rest | Collects remaining properties into an object |
function f(...args) {} | Rest | Collects all arguments into an array |
Best Practices
- Reach for object destructuring when names matter and array destructuring when position matters.
- Use spread for immutable copies and merges instead of
Object.assignmutation orconcat. - Remember spread is shallow — clone or restructure nested objects explicitly when you need deep isolation.
- Prefer rest parameters over the
argumentsobject so you get a real array and arrow-function compatibility. - Use object rest (
const { x, ...others } = obj) to omit keys cleanly rather than deleting them. - Provide destructuring defaults for optional config instead of post-hoc
undefinedchecks. - Default a destructured parameter object (
= {}) so calling the function with no arguments doesn’t throw.