Spread/rest operator and destructuring — how do they work and when do they bite?
Spread expands an iterable/object into elements; rest collects the remainder into an array/object. Destructuring binds positions/keys to variables with optional defaults and renames. All shallow — nested values still share references.
Spread, rest, and destructuring are three sides of the same syntax. Junior developers use them daily; the interview signal is recognizing the shallow-copy trap, the order-of-operations rules, and the subtle differences between array and object forms.
Spread (expand).
const a = [1, 2, 3];
const b = [0, ...a, 4]; // [0, 1, 2, 3, 4]
const o = { x: 1, y: 2 };
const o2 = { ...o, z: 3 }; // { x: 1, y: 2, z: 3 }
fn(...a); // calls fn(1, 2, 3)Array spread works on any iterable (Array, Set, Map, string, generator). Object spread is enumerable own properties only — it skips getters' source descriptors and prototype.
Rest (collect).
const [first, ...rest] = [1, 2, 3]; // first=1, rest=[2,3]
const { a, ...others } = { a: 1, b: 2, c: 3 }; // others={b:2, c:3}
function sum(...nums) { return nums.reduce((a, b) => a + b, 0); }Rest must be the last binding — [...rest, last] is a syntax error.
Destructuring with defaults and renames.
const { name: userName = "Anon", age = 0 } = user;
const [head = 0, ...tail] = arr;
// Defaults apply when value is *undefined* — null does NOT trigger default.The shallow-copy trap (most-asked).
const orig = { user: { name: "A" } };
const copy = { ...orig };
copy.user.name = "B";
console.log(orig.user.name); // "B" — same referenceSpread copies one level. For deep clone use structuredClone(orig) (modern, handles cycles, Maps, Sets) or libraries like Lodash _.cloneDeep. Never JSON.parse(JSON.stringify(...)) on data with Dates, Maps, undefined, or cycles.
Object spread merge order.
const a = { x: 1, y: 2 };
const b = { y: 99, z: 3 };
const merged = { ...a, ...b }; // { x: 1, y: 99, z: 3 } — later winsUse this for "defaults overridden by overrides": { ...DEFAULTS, ...userOptions }.
Array spread vs Array.from. Both turn iterables into arrays. Array.from accepts a mapper (Array.from(set, x => x * 2)); spread does not. Array.from({ length: 5 }) works on array-likes; spread does not.
Performance. Spreading a million-element array is O(n) and allocates. In hot loops prefer mutation (arr.push(x) over arr = [...arr, x]). React state updates should still use immutable patterns — the cost is negligible at typical sizes.
Destructuring + default + computed key.
const key = "name";
const { [key]: who = "?" } = user;Function parameter destructuring is idiomatic for "options bag":
function fetchPage({ cursor, pageSize = 25, signal }: { cursor?: string; pageSize?: number; signal?: AbortSignal } = {}) { ... }The trailing = {} is essential — without it, calling fetchPage() (no arg) throws because you can't destructure undefined.
Code
Follow-up questions
- •Why does default only apply when the value is undefined, not null?
- •Difference between Array.from and array spread?
- •How do you deep-clone an object that contains Dates and Maps?
- •What does { ...null } produce?
Common mistakes
- •Spreading a deeply nested object and assuming you got a deep copy.
- •Using JSON.parse(JSON.stringify(x)) on data with Dates / undefined / functions.
- •Putting rest before other params: function (a, ...b, c) — syntax error.
- •Defaulting on `null` and being surprised: `const { x = 1 } = { x: null }` → x is null.
Performance considerations
- •Repeated spread in a loop (`acc = [...acc, x]`) is O(n²). Use push or reduce.
- •Object spread is O(keys); fine for small objects, expensive for huge ones.
Edge cases
- •Spreading null/undefined into an object is a no-op; into an array throws.
- •Object spread does not copy non-enumerable properties or symbols correctly in all cases (own enumerable only).
- •Destructuring an iterable with [...] consumes a generator entirely.
Real-world examples
- •Redux reducers, React state setters, function options bags, immutable list updates.