Engineering Notes
Home/Languages/JavaScript/Spread and Rest Operators
Languages · JavaScript

Spread and Rest Operators

Both use the same ... syntax but do opposite things depending on context: spread expands a collection into individual elements; rest collects individual elements into a collection.

💻 Languages● Intermediate⏱ 5 min read🛠️ 2 exercises🎤 5 interview Q&As

🎯 What you'll learn

  • Spread in Array Literals
  • Spread in Object Literals
  • Spread in Function Calls
  • Rest in Function Parameters
  • Rest in Destructuring

Both use the same ... syntax but do opposite things depending on context: spread expands a collection into individual elements; rest collects individual elements into a collection.

Spread expands one array into three individual elements; rest collects three individual arguments back into one array
Spread expands one array into three individual elements; rest collects three individual arguments back into one array

1Spread in Array Literals

js
const a = [1, 2];
const b = [3, 4];
const combined = [...a, ...b];        // [1, 2, 3, 4]
const withExtra = [0, ...a, 2.5];      // [0, 1, 2, 2.5]
const copy = [...a];                    // shallow copy, not the same array reference

2Spread in Object Literals

js
const base = { theme: "light", retries: 3 };
const merged = { ...base, retries: 5 }; // { theme: "light", retries: 5 } — later keys win
const copy = { ...base };                // shallow copy

Both array and object spread are shallow: nested objects/arrays inside are still shared by reference (identical caveat to Object.assign, covered in the Objects topic).

3Spread in Function Calls

js
function sum3(a, b, c) { return a + b + c; }
const nums = [1, 2, 3];
sum3(...nums); // 6 — equivalent to sum3(1, 2, 3)

Math.max(...[4, 8, 1]); // 8 — Math.max doesn't accept an array directly, spread fixes that

4Rest in Function Parameters

js
function sum(...numbers) { // collects ALL remaining arguments into a real array
  return numbers.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3, 4); // 10

function logFirst(first, ...rest) { // rest collects everything AFTER named params
  console.log(first, rest);
}
logFirst(1, 2, 3); // 1  [2, 3]

Rest parameters must be the last parameter — function f(...rest, last) {} is a syntax error.

5Rest in Destructuring

js
const [head, ...tail] = [1, 2, 3, 4];   // head=1, tail=[2,3,4]
const { id, ...otherFields } = { id: 1, name: "Ana", age: 30 };
// id=1, otherFields={ name: "Ana", age: 30 }

Same last-position rule applies here too.

6Telling Spread and Rest Apart

Context... means
Inside [...] or {...} literal, or as a function call argumentspread — expand
As a function parameter, or on the left side of destructuringrest — collect
js
function example(...args) {   // rest: args becomes an array
  console.log(Math.max(...args)); // spread: expand args back out for Math.max
}
Under the HoodTwo Genuinely Different Mechanisms Behind One Symbol▾

Array spread and object spread look identical at the syntax level, but they are implemented by completely different internal operations — this is worth knowing precisely, not just as trivia, because it explains what each one can and can't do.

Array spread runs the iterator protocol, for real

js
const arr = [...someIterable];

Spreading inside [...] (or as function-call arguments) literally invokes someIterable[Symbol.iterator]() and calls .next() repeatedly until done: true — the exact mechanism from the Iterators and Generators topic. This is why spread works on any iterable (strings, Map, Set, generators, custom iterable classes), and why spreading a non-iterable value ([...{ a: 1 }], a plain object) throws TypeError: is not iterable — there's no Symbol.iterator method for the engine to call.

js
function* naturalNumbers() { let n = 1; while (n <= 3) yield n++; }
console.log([...naturalNumbers()]); // [1, 2, 3] — spread drives the generator via real .next() calls

Object spread does NOT use iteration at all

js
const merged = { ...base, ...overrides };

{...obj} is specified as an internal CopyDataProperties operation: it directly enumerates obj's own enumerable properties (the same set Object.keys would return) and copies each one — no Symbol.iterator involved whatsoever. This is why object spread works on plain objects (which aren't iterable) but a Map spread with {...someMap} does not copy its entries the way [...someMap] would — {...} only ever sees a Map instance's own enumerable properties, which is essentially none of its actual entries (they're stored internally, not as regular properties — see the Map topic).

Rest parameters allocate a real new array, every call

js
function sum(...numbers) { /* ... */ }

Every single call to sum allocates a fresh array object to hold the collected arguments — this isn't free, though it's cheap in absolute terms and, crucially, it's a real array (unlike the legacy arguments object from the Functions topic), so it doesn't carry that object's historical optimization penalties.

Why spreading enormous arrays into a call can fail

js
fn(...hugeArray); // hugeArray with, say, 200,000 elements

Function calls have an engine-enforced maximum argument count (well into the tens of thousands, but finite) — spread turns each array element into a genuine, individual function argument at the call site, and exceeding that limit throws RangeError: Maximum call stack size exceeded or a similar limit error, because arguments occupy real stack space per the Call Stack topic. Chunking the array or using .apply() with careful batching avoids this; regular iteration (for, .reduce()) never hits the limit because it never turns array elements into call arguments at all.

⚠️ Common Pitfalls

  1. Assuming spread deep-copies — it's shallow, exactly like Object.assign; nested structures remain shared references.
  2. Spreading a Set/Map/string when an array is expected elsewhere — spread works on any iterable, not just arrays: [..."abc"] gives ["a", "b", "c"].
  3. Using rest anywhere but the last position — always a syntax error, whether in function parameters or destructuring patterns.
  4. Spreading huge arrays into function calls — fn(...hugeArray) can hit engine argument-count limits (tens of thousands of elements); use .apply() or chunking for very large collections.
  5. Object spread key order — later spread sources overwrite earlier ones for duplicate keys, left to right; getting the order backwards silently produces the wrong merge result.

📋 Quick Reference

text
[...arr]        → spread: expand array into elements
{...obj}         → spread: expand object into key-values
fn(...args)       → spread: expand array into arguments
function f(...a)   → rest: collect arguments into array
const [x, ...y] = arr    → rest: collect remaining into array
const {x, ...y} = obj     → rest: collect remaining into object

🎤 Interview Q&A

Questions interviewers ask about this topic. Answer out loud first, then open a card to compare.

Q1Spread vs rest: what's the difference?core▾

Same ... syntax, opposite roles: spread expands an iterable/object into elements/properties ([...a, ...b], {...obj, x: 1}, f(...args)); rest collects remaining items into an array/object (function f(...args), const [h, ...t] = arr).

Follow-ups interviewers ask
  • Where can rest appear? Only last in parameter lists or destructuring patterns.
Q2How do you merge objects and what wins?core▾

{...defaults, ...options}: later properties override earlier ones. It's shallow: nested objects are replaced, not merged.

Follow-ups interviewers ask
  • Deep merge? Use a library or structuredClone plus custom merge, beware prototype pollution.
Q3Is spread a deep copy?core▾

No, it copies one level; nested objects/arrays are shared references.

Follow-ups interviewers ask
  • Deep copy option? structuredClone.
Q4What can be spread into arrays vs objects?senior▾

Arrays/arguments accept any iterable (strings, Sets, Maps); object spread copies own enumerable properties from any object (spreading null/undefined is ignored).

Follow-ups interviewers ask
  • Spread a string into an array? [...'hi'] gives ['h','i'] (handles Unicode code points).
Q5What are performance considerations of spread?core▾

Spreading large arrays or objects in loops (e.g. in reduce: {...acc, [k]: v}) creates O(n²) copying. Mutate a local accumulator or use Map when building large structures.

Follow-ups interviewers ask
  • When is immutability worth it? State management where change detection relies on new references.

🛠️ Practice exercises

Each exercise is a file in this topic folder. Code exercises are self-checking: run them and make the checks pass.