Articles

JavaScript Spread vs Rest Operators, Explained

The spread operator (...) expands an iterable into individual elements; the rest operator collects elements back into an array. Same syntax, opposite jobs.

Takina Takina · · 4 min read
A close-up of code on an editor screen

The spread and rest operators both use the same ... syntax, but they do opposite things: spread expands an iterable (an array, string, or object) into its individual elements, while rest collects multiple elements back into a single array. Which one you’re looking at depends entirely on where the ... appears — inside a function call or literal, it’s spread; inside a function’s parameter list or a destructuring pattern, it’s rest.

Spread: expanding a collection

Spread takes something iterable and unpacks it into individual values, wherever a list of values is expected.

In function calls, spread replaces the old Function.prototype.apply trick for passing an array as individual arguments:

const nums = [4, 8, 15];
Math.max(...nums); // same as Math.max(4, 8, 15)

In array literals, spread is the idiomatic way to copy or merge arrays without mutating the originals:

const a = [1, 2];
const b = [3, 4];
const merged = [...a, ...b]; // [1, 2, 3, 4]
const copy = [...a];         // a shallow copy of a

In object literals, spread copies enumerable own properties, and later keys overwrite earlier ones — a compact alternative to Object.assign:

const defaults = { theme: "dark", timeout: 30 };
const overrides = { timeout: 60 };
const config = { ...defaults, ...overrides }; // { theme: "dark", timeout: 60 }

That merge pattern is common in state updates, where you want a new object rather than mutating an existing one — the same instinct behind destructuring assignment, which spread frequently pairs with.

One caveat worth remembering: spread only produces a shallow copy. Nested objects and arrays are still shared by reference between the original and the copy, so mutating a nested property affects both.

Rest: collecting the remainder

Rest does the inverse: it gathers a variable number of remaining elements into a single array. It shows up in two places.

In function parameters, rest collects any arguments beyond the named ones, replacing the old arguments object with a real array:

function sum(first, ...rest) {
  return rest.reduce((total, n) => total + n, first);
}
sum(1, 2, 3, 4); // 10

Unlike arguments, a rest parameter is a genuine Array, so methods like .map(), .filter(), and .reduce() — covered in our guide to map, filter, and reduce — work on it directly without conversion. A rest parameter must also be the last parameter in the list; JavaScript needs to know where the “named” arguments stop and the collection begins.

In destructuring, rest captures whatever properties or elements weren’t explicitly pulled out:

const [first, ...others] = [10, 20, 30];
// first = 10, others = [20, 30]

const { id, ...rest } = { id: 1, name: "Ada", role: "admin" };
// id = 1, rest = { name: "Ada", role: "admin" }

This pattern is especially common for splitting out one or two known fields from an object while forwarding the rest — passing unrecognized props through to a child component, for instance.

Telling them apart

The syntax is identical; the position and direction are what distinguish them.

SpreadRest
DirectionExpands a collection into elementsCollects elements into a collection
Appears inFunction calls, array/object literalsFunction parameters, destructuring patterns
Typical useCopying, merging, passing an array as argsVariadic functions, splitting off “the rest”
Position ruleAnywhere in a listMust be last

A useful mental shortcut: if ... is on the receiving side of an assignment or parameter list, it’s rest. If it’s on the producing side — inside a call, an array literal, or an object literal — it’s spread.

Where this replaced older patterns

Before ES2015 introduced spread and ES2018 extended it to objects, these jobs required more verbose alternatives: Function.prototype.apply() for spreading arrays into calls, Object.assign({}, a, b) for merging objects, .slice(1) combined with arguments for capturing extra parameters, and .concat() for combining arrays. Spread and rest didn’t add new capability so much as give existing patterns a consistent, terse syntax — one of many ergonomics improvements that shaped how modern JavaScript is written, alongside changes like let and const replacing var for variable scoping.

A common gotcha: arrays vs objects vs strings

Spread works on anything iterable, which includes arrays, strings, Map, Set, and NodeLists, but plain objects are not iterable by default. Object spread ({ ...obj }) works through a separate mechanism specific to object literals — it’s not the same iteration protocol as array spread, which is why you can’t use ... to spread an object directly into a function call the way you can an array.

const str = "abc";
console.log([...str]); // ['a', 'b', 'c'] — strings are iterable

function fn(...args) {}
fn(...{ a: 1 }); // TypeError — plain objects aren't iterable

If you need to pass an object’s values as individual arguments, spread Object.values(obj) instead.

The takeaway

Spread and rest share the ... syntax but point in opposite directions: spread unpacks a collection into individual values inside a call or literal, while rest packs individual values back into an array inside a parameter list or destructuring pattern. Once that direction clicks, the rest is mostly muscle memory — spread for copying and merging arrays or objects, rest for variadic functions and pulling the “remaining” fields out of a destructure. Both replaced older, more verbose patterns without adding new runtime capability, which is exactly why they read so naturally once you’re used to them.

Takina Takina · · 5 min read

Promise.all() vs allSettled() vs race() Compared

Promise.all() fails fast, allSettled() waits for every result, and race() returns whichever promise finishes first — how to choose correctly.

#JavaScript #Web Development #Frontend
Takina Takina · · 4 min read

ResizeObserver API Explained: Watching Element Size

The ResizeObserver API lets JavaScript watch an element's box size and react without polling or resize-event hacks. How it works and when to use it.

#Web Development #JavaScript #Frontend
Takina Takina · · 4 min read

localStorage vs sessionStorage vs Cookies

localStorage, sessionStorage, and cookies all store data in the browser, but differ in lifetime, size limits, and whether the server can see them.

#JavaScript #Web Development #Frontend