Destructuring, Spread, and Rest in JavaScript
A complete tour of the ES6 patterns that show up everywhere — object and array destructuring with defaults and renaming, the rest pattern, and spread for copies and merges.
What you'll learn
- ✓Array destructuring with skipping and rest
- ✓Object destructuring with renaming and defaults
- ✓Nested destructuring patterns that stay readable
- ✓Destructuring in function parameters
- ✓Spread for copies, merges, and function calls
- ✓Why all of this is shallow — and when that bites
Destructuring, spread, and rest are three syntactic features added in ES6 that, between them, have reshaped how modern JavaScript looks. They appear in nearly every function signature, every state update, every config merge. None of them add new behaviour to the language — they’re shorter ways to say things you could already say. The savings, accumulated across a codebase, are enormous.
Array destructuring
Pull values out of an array by position:
const point = [3, 4];
const [x, y] = point;
console.log(x, y); // 3 4
The names on the left are new variables; the array on the right is matched element by element.
You can skip elements with commas:
const [, , third] = [10, 20, 30];
console.log(third); // 30
And collect a tail with ...:
const [first, ...rest] = [10, 20, 30, 40];
console.log(first); // 10
console.log(rest); // [20, 30, 40]
Defaults fire when the slot is undefined:
const [a = 1, b = 2, c = 3] = [10, undefined];
console.log(a, b, c); // 10 2 3
A neat application is swapping two variables — no temp variable needed:
let a = 1;
let b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
Array destructuring works on any iterable, including strings:
const [first, second] = "hi";
console.log(first, second); // 'h' 'i'
Object destructuring
Pull values out by key:
const user = { name: "Ada", age: 36, city: "London" };
const { name, age } = user;
console.log(name, age); // 'Ada' 36
Variables match property names by default. To use a different name, rename:
const { name: fullName, city: location } = user;
console.log(fullName, location); // 'Ada' 'London'
Provide a default for missing properties:
const { country = "UK" } = user;
console.log(country); // 'UK' — user.country is undefined, so default used
Combine renaming and defaults:
const { country: nation = "UK" } = user;
console.log(nation); // 'UK'
Collect the leftovers with rest:
const { name: n, ...other } = user;
console.log(n); // 'Ada'
console.log(other); // { age: 36, city: 'London' }
Computed and dynamic keys
You can destructure into a property whose name is held in a variable:
const key = "name";
const { [key]: value } = user;
console.log(value); // 'Ada'
This is rare in everyday code but invaluable when working with dynamic data.
Nested destructuring
Destructuring can match nested structure. The pattern on the left mirrors the shape on the right:
const response = {
status: "ok",
data: {
user: { name: "Ada", age: 36 },
posts: [{ title: "Hello" }, { title: "World" }],
},
};
const {
data: {
user: { name },
posts: [firstPost],
},
} = response;
console.log(name); // 'Ada'
console.log(firstPost); // { title: 'Hello' }
This is powerful but can become unreadable fast. A reasonable rule: one or two levels of nesting in a destructuring pattern, no more. Past that, destructure in steps.
Destructuring in function parameters
The single most useful place for object destructuring is the parameter list. It turns an options object into named parameters:
function createUser({ name, age, role = "member" } = {}) {
return { name, age, role };
}
console.log(createUser({ name: "Ada", age: 36 }));
// { name: 'Ada', age: 36, role: 'member' }
console.log(createUser());
// { name: undefined, age: undefined, role: 'member' }
Two things to notice:
- The trailing
= {}lets you callcreateUser()with no argument at all. Without it, destructuringundefinedthrows a TypeError. - Defaults work per-property, exactly like a normal destructure.
Compare with the positional alternative:
function createUser(name, age, role) { ... }
createUser("Ada", 36, undefined);
Positional calls force the caller to remember order and to pass placeholders for skipped arguments. The destructured form is self-describing at the call site.
Array destructuring in parameters works too, often for callbacks that receive entries:
const counts = { apple: 3, banana: 2, cherry: 5 };
Object.entries(counts).forEach(([name, count]) => {
console.log(`${name}: ${count}`);
});
Try it yourself. Given the array [["Alice", 82], ["Bob", 47], ["Carol", 91]], write a forEach that destructures each pair and prints "X scored Y". Then write a formatScore({ name, score, max = 100 }) function that returns a string like "Alice: 82/100".
Spread
Spread is the same ... token, but used in a value position to expand an iterable or object into its parts.
Spread in arrays
const a = [1, 2, 3];
const b = [0, ...a, 4];
console.log(b); // [0, 1, 2, 3, 4]
const merged = [...a, ...[10, 20]];
console.log(merged); // [1, 2, 3, 10, 20]
const copy = [...a];
console.log(copy); // [1, 2, 3]
Spread in function calls
Pass an array as separate arguments:
console.log(Math.max(...[3, 1, 4, 1, 5])); // 5
function add(a, b, c) { return a + b + c; }
console.log(add(...[1, 2, 3])); // 6
Spread in objects
const user = { name: "Ada", age: 36 };
const copy = { ...user };
console.log(copy); // { name: 'Ada', age: 36 }
const updated = { ...user, age: 37 };
console.log(user); // { name: 'Ada', age: 36 } — unchanged
console.log(updated); // { name: 'Ada', age: 37 }
Later keys override earlier ones, which makes merging defaults trivial:
const defaults = { theme: "light", fontSize: 14, showSidebar: true };
const overrides = { fontSize: 16, showSidebar: false };
const settings = { ...defaults, ...overrides };
console.log(settings);
// { theme: 'light', fontSize: 16, showSidebar: false }
This “immutable update” pattern — produce a new object with one or two changes — is fundamental to modern frameworks like React.
Rest vs spread — same syntax, opposite jobs
The ... token does two things, distinguished by context:
- On the left of an
=(or in a parameter list), it’s rest — collecting loose pieces into one thing. - In a value position (function call, array literal, object literal), it’s spread — expanding one thing into pieces.
const [first, ...rest] = [1, 2, 3]; // rest — collect
const expanded = [0, ...rest]; // spread — expand
function log(label, ...values) { ... } // rest — collect
log("data", ...[1, 2, 3]); // spread — expand
The mirror makes them easy to remember.
The shallow copy trap
The spread operator and Object.assign produce shallow copies. Top-level properties are copied; nested objects and arrays are still shared.
const original = {
name: "Ada",
address: { city: "London" },
};
const copy = { ...original };
copy.address.city = "Paris";
console.log(original.address.city); // 'Paris' — surprise
copy.address is the same object as original.address. Mutating it through either name affects both.
The same applies to arrays:
const matrix = [[1, 2], [3, 4]];
const copy = [...matrix];
copy[0].push(99);
console.log(matrix[0]); // [1, 2, 99]
For a true deep copy, use structuredClone (Node 17+, modern browsers):
const original = { name: "Ada", address: { city: "London" } };
const deep = structuredClone(original);
deep.address.city = "Paris";
console.log(original.address.city); // 'London' — unaffected
This is the single biggest gotcha when adopting spread heavily. Lean on structuredClone whenever you need to defend against shared nested state.
Try it yourself. Predict the output and then run it.
const a = { items: [1, 2, 3] };
const b = { ...a };
b.items.push(4);
const c = structuredClone(a);
c.items.push(99);
console.log(a.items, b.items, c.items);Explain why a and b agree, but c doesn’t.
A small worked example
A reusable settings helper that merges defaults with overrides, accepts partial updates, and never mutates its inputs.
const defaults = {
theme: "light",
fontSize: 14,
showSidebar: true,
shortcuts: { save: "Cmd+S", open: "Cmd+O" },
};
function updateSettings(current, { shortcuts, ...changes } = {}) {
return {
...current,
...changes,
shortcuts: { ...current.shortcuts, ...shortcuts },
};
}
const userSettings = updateSettings(defaults, {
fontSize: 16,
shortcuts: { save: "Ctrl+S" },
});
console.log(defaults.fontSize); // 14 — unchanged
console.log(userSettings.fontSize); // 16
console.log(userSettings.shortcuts.save); // 'Ctrl+S'
console.log(userSettings.shortcuts.open); // 'Cmd+O' — preserved
The function destructures the change set, then spreads it. Notice how shortcuts is handled separately so its nested keys merge instead of replacing the whole sub-object. Without that explicit merge, providing one shortcut would erase the rest.
Recap
You now know:
- Array destructuring matches by position; object destructuring matches by name
- Defaults use
=; renaming useskey: alias; both compose:{ key: alias = fallback } - The rest pattern (
...x) on the left collects what’s left - The spread operator (
...x) on the right expands into pieces - Destructuring in parameter lists is the modern named-arguments pattern
- Add
= {}to destructured object parameters to allow calls with no arguments - Spread is shallow — nested objects and arrays are still shared
- Use
structuredClonewhen you need a deep copy
Next steps
You now have a complete toolkit for synchronous JavaScript. The next two posts open the door to asynchronous code — first promises, then the async/await syntax built on top of them.
→ Next: JavaScript Promises Explained
Questions or feedback? Email codeloomdevv@gmail.com.