Topics

Jump to a topic.

Search questions

Find a question by title, tag, or summary.

What's the difference between spread and rest?

Questions in this chapter (5)
  • Draft
  • easy
  • Askedvery often
  • Verified October 2026

Answer

Where ... appears decides which one it is.

Spread is an expression: it expands something into individual items.

  • In a call or new, or inside an array literal, it iterates its operand with the iteration protocol: f(...args), [...a, ...b]. The operand must be iterable (arrays, strings, Set, Map, generators); a plain object throws TypeError.
  • In an object literal, { ...src } performs CopyDataProperties: it reads src's own enumerable properties (string and symbol keys), using Get, so getters run and the copy gets plain data properties. Inherited and non-enumerable properties are skipped, null and undefined are ignored, and a string spreads as index keys. Later keys overwrite earlier ones, so { ...defaults, ...overrides } lets overrides win.

Rest is a binding: it collects the remainder into a new value.

  • In a parameter list, (first, ...others) gives a real array of the extra arguments. It replaces arguments, works in arrow functions, and is not counted in fn.length.
  • In an array pattern, [head, ...tail] drains the iterator into an array.
  • In an object pattern, { a, ...others } copies every own enumerable property except the listed ones into a new plain object.

A rest element must be last and appear once (SyntaxError otherwise), and it takes no default.

Both are shallow: nested objects are copied by reference. Spread is a copy, not a clone, and it doesn't preserve the prototype, descriptors, or non-enumerable properties.

Example

const sum = (...nums) => nums.reduce((a, b) => a + b, 0); // rest: collect
const args = [1, 2, 3];
console.log(sum(...args, 4)); // spread: expand → 10
console.log([...'hi', ...new Set([1, 1, 2])]); // → [ 'h', 'i', 1, 2 ]

const base = { a: 1, b: 2 };
const merged = { ...base, b: 3, c: 4 }; // later keys win
console.log(merged); // → { a: 1, b: 3, c: 4 }

const { a, ...restProps } = merged; // rest: everything but `a`
console.log(a, restProps); // → 1 { b: 3, c: 4 }
const [head, ...tail] = [1, 2, 3];
console.log(head, tail); // → 1 [ 2, 3 ]

const nested = { inner: { n: 1 } };
const copy = { ...nested };
copy.inner.n = 2; // shared reference
console.log(nested.inner.n); // → 2

Pitfalls

  • Treating spread as a deep copy. Only the top level is new; nested objects and arrays are shared.
  • Spreading a plain object into an array or a call: [...{ a: 1 }] throws TypeError because objects aren't iterable. Only the object-literal form accepts any object.
  • Expecting class instances to survive: { ...new A() } is a plain object holding the own enumerable fields — no prototype, no methods, and accessors become their current values.
  • Spreading a huge array into a call (Math.max(...big)). Each element becomes an argument, and engines cap that. In Node 24 (V8), 200,000 elements already throws RangeError; use a loop or reduce.

Follow-ups

What's the difference between { ...a } and Object.assign({}, a)?
Spread defines new properties on the new object (CreateDataPropertyOrThrow), so setters on the target can't intercept. Object.assign does Set, which calls setters, including inherited ones, on its target.
How do you omit a property immutably?
Rest in a pattern: const { password, ...safe } = user. safe is a new object without password.
Is [...arr] the same as arr.slice()?
For dense arrays, yes: both are shallow copies. Spread uses the iterator, so it turns holes into undefined; slice keeps them as holes.

Exercises

Predict the output

const defaults = { theme: 'light', size: 'm' };
const user = { size: undefined, theme: 'dark' };
console.log({ ...defaults, ...user });

Fix the bug

const state = { user: { name: 'Ada' }, count: 0 };
const next = { ...state, count: 1 };
next.user.name = 'Grace';
console.log(state.user.name);