Lesson 20 of 28
Destructuring and Spread
Destructuring, defaults, rest and spread across arrays, objects and parameters — plus the shallow-copy trap where editing your copy quietly rewrites the original.
The expensive bug here is not a syntax error. It is copying an object, editing the copy, and finding that the original changed too. Destructuring and spread read so cleanly that you end up trusting them further than they go.
Pulling values out of arrays
Position decides what you get. Holes are allowed, and a default covers a missing slot.
const scores = [88, 74];
const [first, second, third = 0] = scores;
// 88, 74, 0
const [, runnerUp] = scores; // skip the firstYou have used this without noticing every time you wrote
const [value, setValue] = useState(). It is why hook results are arrays: you
choose the names.
Swapping needs no temporary variable. Start the line with a semicolon, since the previous line might otherwise join up with the bracket:
let current = "draft";
let next = "published";
;[current, next] = [next, current];Pulling values out of objects
Name decides what you get, so order is irrelevant.
const lesson = {
title: "Modules",
minutes: 11,
author: { name: "Priya", city: "Chennai" },
};
const { title, minutes } = lesson;
const { title: heading } = lesson; // rename
const { level = "intermediate" } = lesson; // default
const { author: { name, city } } = lesson; // nested
const { author: { country = "IN" } = {} } = lesson; // nested with a guardTwo things to be precise about. Renaming reads backwards the first few times:
title: heading means take title, call it heading. And defaults fire only
on undefined — a stored null will come through as null and break the code
you expected the default to protect.
Nested destructuring throws if the middle object is missing, which is why that
last line adds = {}. Past two levels, stop and use optional chaining instead.
Destructuring parameters
This is the pattern worth internalising. Compare a positional signature with an options object:
createLesson("Modules", 11, true, false, "intermediate");
createLesson({
title: "Modules",
minutes: 11,
published: true,
level: "intermediate",
});The second is readable at the call site and survives new arguments being added. Destructure it in the signature with defaults, and give the whole parameter a default so calling with no arguments still works:
function createLesson({
title,
minutes = 10,
published = false,
level = "beginner",
} = {}) {
return { title, minutes, published, level };
}The same trick makes callbacks tidier:
users.map(({ name, email }) => `${name} <${email}>`);Rest: collecting what is left
In destructuring, ... gathers the remainder:
const [featured, ...others] = lessons;
const { password, ...safeUser } = user; // omit a field from a copyThat second line is the standard way to strip a property without mutating — handy before logging a user record or sending it to the client.
In a function signature, ... collects extra arguments into a real array:
function track(event, ...details) {
console.log(event, details.join(", "));
}
track("lesson_started", "closures", "13min");Rest must be last in both cases.
Spread: copying and merging
The same three dots in the other direction, spreading a value out.
const base = [1, 2];
const more = [...base, 3]; // [1, 2, 3]
const joined = [...base, ...more]; // [1, 2, 1, 2, 3]
const defaults = { theme: "light", fontSize: 14 };
const settings = { ...defaults, theme: "dark" }; // later keys winOrder matters for objects: the last occurrence of a key wins, so put overrides
after defaults. Spread also works for arguments — Math.max(...scores) — and
turns any iterable into an array, including a Set, which is the one-line way
to remove duplicates: [...new Set(tags)].
Two details worth knowing. Spreading undefined into an object is a no-op
rather than an error, which makes conditional properties tidy:
const query = {
page: 1,
...(search ? { q: search } : {}),
};And object spread is not quite Object.assign. Both copy own enumerable
properties, but Object.assign writes into an existing object and triggers any
setters on the target, while spread builds a fresh plain object. When you only
want a copy, spread is the safer default.
Rest and spread combine naturally in a function that takes options:
function createLesson({ title, ...rest }) {
return { title, slug: title.toLowerCase().split(" ").join("-"), ...rest };
}Try it yourself
Every one of these copies is shallow
Spread copies the values of the top-level properties. When a value is an object or an array, the value is the reference. Both objects then point at the same nested thing.
const defaults = { theme: "light", editor: { fontSize: 14 } };
const user = { ...defaults, theme: "dark" };
user.editor.fontSize = 18;
console.log(defaults.editor.fontSize); // 18 — the original changed
console.log(user.editor === defaults.editor); // trueThis is the bug that eats afternoons. You reset a form to its defaults and the defaults have already been edited. You clone a cart line to preview a discount and the real cart shows the discount. In React it is worse in a quieter way: you mutate a nested object inside a copied state object, the top-level reference is new so a re-render happens, and the change appears to work — until a component that compares the nested object decides nothing changed.
There are two honest ways out. Copy every level you intend to touch:
const user = {
...defaults,
editor: { ...defaults.editor, fontSize: 18 },
};Or take a real deep copy when the shape is unknown or deep:
const snapshot = structuredClone(defaults);structuredClone handles nested objects, arrays, Map, Set and Date. It
throws on functions and DOM nodes. The old JSON.parse(JSON.stringify(x))
trick works but silently turns dates into strings and drops undefined, so
prefer structuredClone.
Immutable updates
React state, reducers and most state libraries require a new reference rather than an edit in place. These four cover nearly everything.
// add
const withNew = [...items, newItem];
// remove
const without = items.filter((item) => item.id !== id);
// update one item
const updated = items.map((item) =>
item.id === id ? { ...item, done: true } : item
);
// update a nested field
const next = {
...order,
customer: { ...order.customer, city: "Pune" },
};Note the third one. Copying the array alone is not enough — [...items] gives
you a new array holding the same item objects, so mutating one of them is still
visible everywhere. Replace the item, do not edit it.
What to remember
- Destructuring names things at the point of use; defaults apply only to
undefined. - The options object with destructured parameters and defaults is the signature to reach for.
- Rest collects, spread expands, and rest must come last.
- Spread copies one level deep. Copy each level you change, or use
structuredClone.
Check yourself
4 questions · pass 3/4 to unlock Modules
1.You spread an object that has a nested
prefsobject, then setcopy.prefs.theme = "light". What isoriginal.prefs.themenow?2.When does the default in
const { total = 0 } = orderapply?3.Which is true of
restinfunction tag(first, ...rest) {}?4.You need to mark one item in a state array as done without mutating the array. Which is right?
4 left to answer