JavaScript Spread & Rest

6 minute read

The ... token does two opposite-looking jobs in JavaScript. As spread it unpacks an iterable or object into another expression; as rest it gathers multiple things into one. Same three dots, different position. Together they handle a huge slice of everyday array, object, and function work — copying, merging, variable arity, and partial application.

The Two Faces of ...

Position Role Example Meaning
Inside an array/object literal or call args spread [...a] f(...args) { ...obj } unpack
Inside a destructuring pattern or parameter list rest [a, ...b] ({a, ...b}) (a, ...b) => gather
// Spread — unpacks
const nums = [1, 2, 3];
console.log(...nums);                  // 1 2 3 (three arguments to console.log)

// Rest — gathers
function sum(...args) { return args.reduce((a, b) => a + b, 0); }
sum(1, 2, 3);                          // 6

Spread for Arrays

Copy

const a = [1, 2, 3];
const b = [...a];

a === b;       // false — different arrays
a[0] === b[0]; // true (shallow copy)

Shallow — nested objects are still shared:

const a = [{ x: 1 }];
const b = [...a];
b[0].x = 99;
console.log(a[0].x);   // 99 — same nested object

Concat

const a = [1, 2];
const b = [3, 4];

[...a, ...b];          // [1, 2, 3, 4]
[...a, 99, ...b];      // [1, 2, 99, 3, 4]
[0, ...a, ...b, 99];   // [0, 1, 2, 3, 4, 99]

More readable than a.concat(b).

From Iterable to Array

... works on any iterable — strings, Set, Map, NodeList, generator, arguments…

[..."abc"];                 // ["a", "b", "c"]
[...new Set([1, 2, 2, 3])]; // [1, 2, 3]  — dedupe trick!
[...document.querySelectorAll("li")];

// Equivalent: Array.from(x)
Array.from("abc");

A famous one-liner: dedupe an array.

const unique = [...new Set([1, 2, 2, 3, 3, 3])];
// [1, 2, 3]

Spread Into Function Calls

Replaces the old apply trick:

const nums = [5, 1, 9, 3];

Math.max(...nums);             // 9
Math.max.apply(null, nums);    // 9, old form

console.log(...["a", "b"]);    // "a" "b"

Useful for forwarding arguments:

function logCall(fn, ...args) {
  console.log("calling with", args);
  return fn(...args);
}

Spread for Objects

const a = { x: 1, y: 2 };
const b = { y: 99, z: 3 };

// Copy
const copy = { ...a };

// Merge — later keys win
const merged = { ...a, ...b };
// { x: 1, y: 99, z: 3 }

// Add/override
const updated = { ...a, y: 100, w: 4 };
// { x: 1, y: 100, w: 4 }
  Object spread Object.assign({}, ...)
Readability high medium
Triggers setters on target no yes
Recommended ✅ default only when you need setters

Warning: Object spread is shallow — nested objects are shared. To deep clone use structuredClone(obj).

const original = { profile: { age: 30 } };
const copy = { ...original };
copy.profile.age = 99;
console.log(original.profile.age);   // 99 — same nested object!

Spread + Default for Immutable Updates

The Redux / React pattern for “update one key”:

const state = { user: "Ada", count: 0, active: true };

const next = { ...state, count: state.count + 1 };
// state is unchanged; next is { user: "Ada", count: 1, active: true }

Nested updates need explicit nested spread:

const state = { user: { name: "Ada", age: 30 } };

const next = {
  ...state,
  user: { ...state.user, age: 31 },
};

Rest in Function Parameters

Collect any number of trailing arguments into an array:

function tag(label, ...items) {
  return `${label}: ${items.join(", ")}`;
}

tag("fruits", "apple", "banana");      // "fruits: apple, banana"
tag("empty");                          // "empty: "
tag("one", "two", "three", "four");    // "one: two, three, four"

Rules:

  • ✅ Rest must be last in the parameter list
  • ✅ Rest is always an array (never null/undefined)
  • ✅ Use instead of the legacy arguments object (which doesn’t exist on arrows anyway)
function bad(...args, last) {}    // ❌ SyntaxError
function ok(first, ...args) {}    // ✅

Rest in Destructuring

Already met in chapter 33 — gather the leftovers.

// Array
const [head, ...tail] = [1, 2, 3, 4];
// head = 1, tail = [2, 3, 4]

// Object
const { name, ...rest } = { name: "Ada", age: 37, role: "admin" };
// name = "Ada", rest = { age: 37, role: "admin" }

A common pattern: “everything except these fields”

function publicView(user) {
  const { password, internalId, ...safe } = user;
  return safe;
}

publicView({ id: 1, name: "Ada", password: "secret", internalId: "x" });
// { id: 1, name: "Ada" }

Spread + Rest Together — Forwarding

function withTiming(fn) {
  return (...args) => {                  // rest: gather
    const start = performance.now();
    const result = fn(...args);          // spread: forward
    console.log(`${fn.name} took ${performance.now() - start} ms`);
    return result;
  };
}

const slowSum = withTiming((a, b) => {
  for (let i = 0; i < 1e6; i++) {}       // pretend it's slow
  return a + b;
});

slowSum(2, 3);   // logs the timing → 5

...args gathers, fn(...args) spreads. The pair is the canonical higher-order wrapper.

Cloning vs Referencing — Cheatsheet

// Shallow clone
const arrCopy = [...arr];
const objCopy = { ...obj };

// Deep clone (modern, ES2022+)
const deep = structuredClone(obj);

// Deep clone (JSON-safe only — loses functions, dates → strings)
const deepJson = JSON.parse(JSON.stringify(obj));

// In-place identity check
arr === arr;          // true
arr === [...arr];     // false

A Realistic Example — Config Builder

const DEFAULTS = Object.freeze({
  timeout: 5000,
  retries: 3,
  headers: { "Content-Type": "application/json" },
});

function request(url, opts = {}) {
  // Merge top-level + deep-merge headers
  const config = {
    ...DEFAULTS,
    ...opts,
    headers: { ...DEFAULTS.headers, ...(opts.headers ?? {}) },
  };

  console.log("→ fetching", url, "with", config);
  // (actual fetch here)
}

request("/api", { retries: 5, headers: { "X-Trace": "abc" } });
// → fetching /api with
//   { timeout: 5000, retries: 5,
//     headers: { Content-Type: ..., X-Trace: "abc" } }

Note the explicit nested merge for headers — a flat spread alone would have replaced the whole object.

Common Mistakes

// ❌ Trying to spread null / undefined as object → silently treated as {}
const merged = { ...null };   // {}   no error
const arr = [...null];        // ❌ TypeError — null is not iterable

// ❌ Spread inside a non-array/object context
const x = ...arr;             // ❌ SyntaxError

// ❌ Rest in the middle
function f(a, ...mid, b) {}   // ❌ SyntaxError — rest must be last

// ❌ Expecting deep clone from spread
const a = { nested: { x: 1 } };
const b = { ...a };
b.nested.x = 99;
// a.nested.x is also 99

// ❌ Performance — spreading huge arrays in tight loops
let acc = [];
for (const chunk of bigChunks) {
  acc = [...acc, ...chunk];   // O(n²) — copies the whole acc each time
}
// ✅
const acc = [];
for (const chunk of bigChunks) acc.push(...chunk);

Best Practices Checklist

  • ✅ Use spread to clone ([...arr], { ...obj }) and merge — readable and standard
  • ✅ Use rest in parameters instead of arguments
  • ✅ Use rest in destructuring to express “everything except these”
  • ✅ Use structuredClone for deep copies
  • ✅ Cap object iterables — spread on iterables is O(n)
  • ❌ Don’t assume spread is deep — it’s shallow
  • ❌ Don’t put rest in the middle of a parameter list
  • ❌ Don’t repeatedly rebuild large arrays with spread — push instead

What’s Next?

You’ve mastered the building blocks of JS data. Time to serialise and exchange it — the language’s universal data format and its built-in JSON object.

← Previous: JS Destructuring Next: JS JSON →

Updated: