Back to JavaScript
JavaScript
easy
junior

How do the spread and rest operators and destructuring work, and where do they bite you?

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.

5 min read·~8 min to think through

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).

ts
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).

ts
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.

ts
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).

ts
const orig = { user: { name: "A" } };
const copy = { ...orig };
copy.user.name = "B";
console.log(orig.user.name); // "B" — same reference

Spread 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.

ts
const a = { x: 1, y: 2 };
const b = { y: 99, z: 3 };
const merged = { ...a, ...b }; // { x: 1, y: 99, z: 3 } — later wins

Use 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.

ts
const key = "name";
const { [key]: who = "?" } = user;

Function parameter destructuring is idiomatic for "options bag":

ts
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

ts
// Immutable update
const next = { ...state, items: [...state.items, newItem] };

// Omit a key
const { password, ...safeUser } = user;

// Conditional spread
const cfg = { ...base, ...(isProd && { logger: prodLogger }) };

// Array clone
const copy = [...arr];

// Convert NodeList to array
const els = [...document.querySelectorAll(".item")];
Common patterns

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.

Senior engineer discussion

Senior signal: shallow-copy awareness, structuredClone over JSON tricks, hot-path performance, and the null-vs-undefined default rule.