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.



