Skip to main content

Command Palette

Search for a command to run...

Spread and Rest Operators in JavaScript: Expanding and Collecting Values

Published
11 min read
Spread and Rest Operators in JavaScript: Expanding and Collecting Values

Two of the most useful features introduced in ES6 share identical syntax: three dots (...). One expands values out. The other collects values in. Understanding which is which — and where each one applies — makes a large set of everyday JavaScript patterns feel natural.

This article covers both operators completely: what they do, how they differ, and where they show up in real code.


The Core Idea: Expanding vs Collecting

The three-dot syntax does one of two opposite things depending on context:

  • Spread takes something iterable and expands it into individual elements. Many becomes many.

  • Rest takes individual elements and collects them into a single array. Many becomes one.

Spread:  [1, 2, 3]  →  1, 2, 3   (array expands into separate values)
Rest:    1, 2, 3    →  [1, 2, 3]  (separate values collected into array)

The context tells you which one you are looking at. Inside a function call or an array/object literal, ... is spread. In a function parameter list or a destructuring pattern, ... is rest.


The Spread Operator

The spread operator expands an iterable — an array, string, or any object with a Symbol.iterator — into individual elements where separate values are expected.

Spreading into an array

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

const combined = [...a, ...b];
console.log(combined); // [1, 2, 3, 4, 5, 6]

Without spread, [a, b] would give you an array of two arrays: [[1, 2, 3], [4, 5, 6]]. Spread unpacks each array so its elements become direct members of the new array.

You can mix spread elements with literal values in any order:

const nums = [2, 3, 4];

const withBoundaries = [1, ...nums, 5];
console.log(withBoundaries); // [1, 2, 3, 4, 5]

Spreading into a function call

A function call expects individual arguments. Spread lets you pass an array where a list of arguments is expected:

function add(x, y, z) {
  return x + y + z;
}

const values = [10, 20, 30];
add(...values); // 60

Before spread, this required Function.prototype.apply:

add.apply(null, values); // 60 — the old way

Spread is cleaner and more readable. It also works anywhere in the argument list:

Math.max(...[3, 1, 7, 2]); // 7
Math.min(0, ...[3, 1, 7]); // 0

Spreading a string

Strings are iterable, so spread works on them too — each character becomes a separate element:

const chars = [..."hello"];
console.log(chars); // ["h", "e", "l", "l", "o"]

This is a clean way to split a string into characters, and unlike split(""), it handles Unicode code points correctly.


Spread with Objects

The spread operator also works with objects (using the object spread syntax, not the iterable protocol). It copies enumerable own properties from one object into another.

Copying an object

const original = { name: "Priya", age: 28 };
const copy = { ...original };

copy.age = 30;

console.log(original.age); // 28 — not affected
console.log(copy.age);     // 30

This creates a shallow copy. Primitive values (name, age) are copied by value. Nested objects would still be shared references.

Merging objects

const defaults = { theme: "light", language: "en", fontSize: 14 };
const userPrefs = { theme: "dark", fontSize: 16 };

const settings = { ...defaults, ...userPrefs };
console.log(settings);
// { theme: "dark", language: "en", fontSize: 16 }

Properties that appear later in the spread override earlier ones. userPrefs spreads after defaults, so theme and fontSize from userPrefs win. language comes only from defaults, so it remains.

This makes object spread the clean way to apply overrides — default configuration merged with user-specific overrides.

Adding or overriding specific properties

const user = { name: "Rahul", role: "user", active: true };

const adminUser = { ...user, role: "admin" };
console.log(adminUser);
// { name: "Rahul", role: "admin", active: true }

Spread user first, then specify the override. Property order in the literal determines which value wins — later properties override earlier ones.


The Rest Operator

Rest does the opposite of spread. It collects multiple values into a single array. It appears in two places: function parameter lists and destructuring patterns.

Rest parameters in functions

Rest parameters let a function accept any number of arguments and collect the extras into an array:

function sum(...numbers) {
  return numbers.reduce((total, n) => total + n, 0);
}

sum(1, 2, 3);       // 6
sum(10, 20);        // 30
sum(1, 2, 3, 4, 5); // 15

numbers is always a real array — you can call reduce, map, filter, and any other array method on it directly.

Before rest parameters, arguments was the tool for this:

function sum() {
  return Array.from(arguments).reduce((total, n) => total + n, 0);
}

arguments has two problems: it is not a real array (no array methods without conversion), and it does not work inside arrow functions. Rest parameters solve both.

Rest must be last

The rest parameter collects "everything that remains," so it can only appear at the end of the parameter list:

function log(level, timestamp, ...messages) {
  console.log(`[\({level}] \){timestamp}:`, messages.join(" | "));
}

log("INFO", "10:30", "Server started", "Port 3000", "Ready");
// [INFO] 10:30: Server started | Port 3000 | Ready

level and timestamp capture the first two arguments. messages collects everything after. This pattern — specific named parameters first, then rest — is common for utility and logging functions.

Rest in array destructuring

Rest in a destructuring pattern collects the remaining elements after specific ones have been extracted:

const [first, second, ...remaining] = [10, 20, 30, 40, 50];

console.log(first);     // 10
console.log(second);    // 20
console.log(remaining); // [30, 40, 50]

remaining is always an array — even if there is only one element left, or none at all:

const [head, ...tail] = [1];
console.log(head); // 1
console.log(tail); // []

Rest in object destructuring

Rest in object destructuring collects the properties that were not explicitly extracted:

const user = { name: "Priya", age: 28, role: "admin", active: true };

const { name, role, ...rest } = user;

console.log(name); // "Priya"
console.log(role); // "admin"
console.log(rest); // { age: 28, active: true }

rest is a new object containing only the properties that were not named explicitly. This is how you "pick" properties from an object without mutating it.


Spread vs Rest: Side by Side

Same syntax, opposite directions:

Spread Rest
Direction Expands one into many Collects many into one
Where it appears Array literals, object literals, function calls Function parameters, destructuring patterns
Input Iterable or object Multiple values or remaining elements
Output Individual elements A single array or object
// Spread — one array expands into individual arguments
Math.max(...[3, 1, 4, 1, 5]); // 5

// Rest — individual arguments collected into one array
function max(...nums) {
  return Math.max(...nums); // spread again inside
}
max(3, 1, 4, 1, 5); // 5

Notice the symmetry: max uses rest to collect the arguments into nums, then uses spread to pass them to Math.max. Collect, then expand.


Practical Use Cases

Cloning an array

const original = [1, 2, 3];
const clone = [...original];

clone.push(4);

console.log(original); // [1, 2, 3] — unaffected
console.log(clone);    // [1, 2, 3, 4]

This replaces original.slice() with something more readable. The clone is a new array — mutations do not propagate back to original.

Merging arrays

const frontend = ["React", "CSS", "TypeScript"];
const backend  = ["Node.js", "PostgreSQL", "Redis"];

const fullStack = [...frontend, ...backend];
// ["React", "CSS", "TypeScript", "Node.js", "PostgreSQL", "Redis"]

As many arrays as you like, in any order, with optional literal values between them.

Converting a Set to an array

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

console.log(arr); // [1, 2, 3, 4]

Set is iterable, so spread works. This is the cleanest way to deduplicate an array:

const deduped = [...new Set([1, 2, 2, 3, 3, 4])];

Converting a NodeList to an array

When you query the DOM, querySelectorAll returns a NodeList, not a real array. Spread converts it:

const elements = [...document.querySelectorAll(".card")];
elements.filter(el => el.classList.contains("active")); // now you can use array methods

Building modified copies of objects

In React and other state-management patterns, you need to update objects without mutating the original. Spread makes this clean:

const user = { name: "Priya", age: 28, city: "Mumbai" };

// Update one field
const older = { ...user, age: 29 };

// Add a new field
const withEmail = { ...user, email: "priya@example.com" };

// Remove a field (using rest destructuring)
const { city, ...withoutCity } = user;

All three patterns produce new objects without touching the original. This is the pattern used constantly in Redux reducers and React state updates.

Passing dynamic arguments

When you have arguments that may vary in count, spread lets you handle them uniformly:

function formatMessage(template, ...values) {
  return template.replace(/\{(\d+)\}/g, (_, i) => values[i] ?? "");
}

formatMessage("Hello, {0}! You have {1} messages.", "Priya", 5);
// "Hello, Priya! You have 5 messages."

Rest collects the substitution values, and the function handles any number of them.

Merging default configuration with overrides

This is one of the most common real-world uses of object spread:

function createRequest(options) {
  const defaults = {
    method: "GET",
    headers: { "Content-Type": "application/json" },
    timeout: 5000,
    retry: true
  };

  return { ...defaults, ...options };
}

createRequest({ method: "POST", timeout: 10000 });
// { method: "POST", headers: {...}, timeout: 10000, retry: true }

defaults provides a baseline. options overrides only what the caller specifies. Everything else remains at the default value.

Collecting remaining items after destructuring

function processOrder({ id, status, ...details }) {
  console.log(`Order \({id} is \){status}`);
  console.log("Additional details:", details);
}

processOrder({
  id: "ORD-001",
  status: "shipped",
  customer: "Priya",
  address: "Mumbai",
  total: 1499
});
// Order ORD-001 is shipped
// Additional details: { customer: "Priya", address: "Mumbai", total: 1499 }

The function extracts what it needs and passes the rest along without having to enumerate every property.


Things to Watch Out For

Spread creates shallow copies

Spread copies references for nested objects, not the objects themselves:

const original = { name: "Priya", address: { city: "Mumbai" } };
const copy = { ...original };

copy.address.city = "Delhi"; // mutates the nested object

console.log(original.address.city); // "Delhi" — original affected

copy.address and original.address point to the same object. If you need a deep copy, you need a different approach — structuredClone(), a recursion, or a library.

Rest only works at the end

In both function parameters and destructuring, rest must be the last element. Placing it elsewhere is a syntax error:

function bad(...args, last) { } // SyntaxError

const [...rest, last] = [1, 2, 3]; // SyntaxError

Spread does not deep-merge objects

When two spread objects share a nested property, the second replaces the first entirely — it does not merge them:

const a = { config: { debug: true, verbose: false } };
const b = { config: { verbose: true } };

const merged = { ...a, ...b };
console.log(merged.config); // { verbose: true } — debug is gone

b.config overwrites a.config completely. If you need recursive merging, use a utility like lodash.merge or write a deep merge function.


Quick Reference

// Spread into array
[...arr]
[...arr1, ...arr2]
[value, ...arr, value]

// Spread into function call
fn(...arr)
Math.max(...numbers)

// Spread into object
{ ...obj }
{ ...defaults, ...overrides }
{ ...obj, key: newValue }

// Rest parameters
function fn(...args) {}
function fn(first, second, ...rest) {}

// Rest in array destructuring
const [a, b, ...rest] = arr;
const [head, ...tail] = arr;

// Rest in object destructuring
const { key1, key2, ...rest } = obj;

Wrapping Up

The ... syntax is one of the most versatile additions to modern JavaScript. The key is reading it directionally: spread pushes values out of a container; rest pulls values into one.

Once you see it that way, the patterns follow naturally. Cloning an array, merging objects, collecting variadic function arguments, skipping specific destructured values — all of these use the same two ideas applied in different positions.

The practical benefit is code that is shorter, clearer, and easier to reason about. A spread merge communicates intent directly. A rest parameter eliminates the need to document that a function takes a variable number of arguments. The three dots do a lot of heavy lifting.

More from this blog