JavaScript Intermediate #4 Destructuring and spread/rest in Depth
In Basics #5 Objects and Arrays you saw destructuring and spread lightly. This post takes those tools deeper — into patterns you meet often in practice.
Object destructuring — basics + variations #
const user = { id: 'u1', name: 'Curtis', age: 30 };
// basic
const { name, age } = user;
// rename
const { name: userName } = user;
// default
const { email = 'none' } = user;
// rename + default
const { phone: userPhone = 'unset' } = user;The last pattern is confusing at first — receive the phone key as userPhone, defaulting to 'unset' if missing. Common in functions taking option objects.
Unpacking nested objects #
const response = {
data: {
user: {
id: 'u1',
profile: {
name: 'Curtis',
age: 30,
},
},
},
};
const {
data: {
user: {
profile: { name, age },
},
},
} = response;
console.log(name, age); // Curtis 30
Pull values from a deeply nested place in one line. No intermediate variables (data, user) — only the leaf values you need.
But going too deep hurts readability. When nesting goes two-plus levels, it’s usually clearer to split into two or three steps or use optional chaining (#5).
Array destructuring — patterns #
const arr = [1, 2, 3, 4, 5];
const [a, b] = arr; // a=1, b=2
const [, , c] = arr; // c=3 (skip first two)
const [first, ...rest] = arr; // first=1, rest=[2,3,4,5]
const [x = 10] = []; // x=10 (default)
Order is meaningful in arrays — destructuring receives by order.
Swap — exchange two variables #
let a = 1, b = 2;
[a, b] = [b, a];
console.log(a, b); // 2 1
A one-line idiom without a temp variable. The old way was let temp = a; a = b; b = temp;.
Parameter destructuring — most common case #
Destructuring in function parameters is one of modern JavaScript’s most common idioms.
function createUser({ name, age, email = 'unset' }) {
console.log(name, age, email);
}
createUser({ name: 'Curtis', age: 30 });
// Curtis 30 unset
The pattern’s strengths:
- Key names visible at the call site — no need to remember argument order
- Optional arguments expressed via key + default
- Adding new arguments preserves compatibility — old calls don’t break
A React component’s props is exactly this pattern.
Allow calling with no object #
function init({ debug = false, retries = 3 } = {}) {
console.log(debug, retries);
}
init(); // false 3 — OK without arguments
init({ debug: true }); // true 3
Adding = {} to the parameter itself starts with an empty object when missing, making destructuring safe. The key pattern when all options are optional.
Take some, gather the rest #
function update(user, { id, ...rest }) {
console.log(id); // separately
console.log(rest); // remaining object
Object.assign(user, rest);
}
update(targetUser, { id: 'u1', name: 'Curtis', age: 30 });
// id: u1
// rest: { name: 'Curtis', age: 30 }
Extract id for inspection/log/special handling, and collect the rest as an object to pass via spread/assign. Common in library functions.
Spread variations — objects #
1) No deep merge (shallow copy) #
const a = { user: { name: 'Curtis', age: 30 } };
const b = { ...a, theme: 'dark' };
b.user.age = 31;
console.log(a.user.age); // 31 ← a changes too
{...a} creates only first-level properties anew. Inner objects still share the same reference (Basics #2). For deep copy:
const a = { user: { name: 'Curtis', age: 30 } };
const b = structuredClone(a);
b.user.age = 31;
console.log(a.user.age); // 30
structuredClone is a built-in standardized in ES2022. It deep-copies most data structures (objects, arrays, Date, Map, Set, etc.). The old trick JSON.parse(JSON.stringify(...)) couldn’t handle functions/Date/undefined.
2) Conditional properties — && and spread
#
const includeEmail = true;
const profile = {
name: 'Curtis',
age: 30,
...(includeEmail && { email: 'me@example.com' }),
};If includeEmail is true, { email: ... } is spread; if false, an empty object is spread (nothing happens).
A pattern often used to build options objects dynamically.
3) Spread into function arguments #
const args = [1, 2, 3];
Math.max(...args); // 3
function logAll(a, b, c) {
console.log(a, b, c);
}
logAll(...args); // 1 2 3
Modern replacement for apply. The old way was Math.max.apply(null, args).
Spread vs Rest — same ..., different meaning
#
The same ... syntax means different things by position.
| Position | Name | Meaning |
|---|---|---|
| Function call argument | spread | unpack array as arguments |
| Inside array/object literal | spread | unpack elements/properties |
| Function parameter | rest | gather arguments into array |
| In a destructuring pattern | rest | gather the rest |
const arr = [1, 2, 3];
Math.max(...arr); // spread — unpack
const copy = [...arr]; // spread — unpack
function f(...args) {} // rest — gather
const [head, ...tail] = arr; // rest — gather
Gather on the left → rest; unpack on the right → spread. The shortest mnemonic.
Common practical patterns #
1) immutable update — change one field, new object #
const user = { id: 'u1', name: 'Curtis', age: 30 };
const updated = { ...user, age: 31 };
// original untouched, only age updated
Essential in React setState-like cases.
2) Array — update one element by index #
const items = ['a', 'b', 'c', 'd'];
const i = 2;
const newValue = 'C';
const updated = [
...items.slice(0, i),
newValue,
...items.slice(i + 1),
];
// ['a', 'b', 'C', 'd']
// or ES2023's toSpliced
const updated2 = items.toSpliced(i, 1, newValue);toSpliced is shorter. The trend in modern JavaScript is more immutable methods.
3) Merging keys — combine two objects #
const defaults = { theme: 'light', lang: 'ko' };
const userPrefs = { theme: 'dark' };
const merged = { ...defaults, ...userPrefs };
// { theme: 'dark', lang: 'ko' }
The later object wins on shared keys (priority). Same effect as Object.assign({}, defaults, userPrefs).
4) JSX/React props pass-through #
function Wrapper(props) {
return <Child {...props} />;
}Very common in React code. A case where the parent passes received props through to a child.
Pitfall — losing prototype info on object spread #
{...obj} takes only obj’s own enumerable properties. The prototype chain isn’t followed.
class User {
constructor(name) {
this.name = name;
}
greet() {
console.log(`hi, ${this.name}`);
}
}
const u = new User('Curtis');
const copy = { ...u };
u.greet(); // hi, Curtis
copy.greet(); // ✗ TypeError — copy is plain, no greet
Spread-copying a class instance produces a plain object. Methods live on the prototype and disappear. Object.assign has the same limit, and structuredClone doesn’t preserve class instances either (turns into a plain object). For real instance copies, give the class its own clone() method is typical.
Wrap-up #
What we covered:
- Object destructuring — rename, default, combined, nested
- Array destructuring — swap idiom, rest for head/tail
- Parameter destructuring — options-object pattern is most common
= {}to allow argument-less calls- spread is shallow — deep copy via
structuredClone - Conditional properties —
...(cond && { ... }) - spread vs rest — left rest / right spread
- Immutable update patterns
- Class instances can’t be copied via spread
In the next post (#5 Optional Chaining and Nullish Coalescing) we cover safely accessing deep objects with ?. and the ?? operator that distinguishes nullish from falsy.