Spread vs Rest Operators in JavaScript

Same syntax. Opposite jobs. One expands values outward, the other collects them inward.
The Syntax That Does Two Opposite Things
JavaScript has a three-dot syntax, ..., that appears in two completely different contexts and does two completely different things. This trips up a lot of developers because the dots look identical whether they're spreading or resting.
// Spread — expands an array into individual values
const nums = [1, 2, 3];
console.log(...nums); // 1 2 3 (three separate values)
// Rest — collects individual values into an array
function sum(...nums) {
return nums.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // nums inside = [1, 2, 3]
Same three dots. Opposite direction of data flow.
The simplest way to remember them:
Spread: [1, 2, 3] → 1, 2, 3 (one thing → many things)
Rest: 1, 2, 3 → [1, 2, 3] (many things → one thing)
The Spread Operator: Expanding Values Outward
The spread operator takes an iterable, an array, a string, or an object, and expands it into its individual elements. Think of it as unpacking a box: the box disappears, and everything that was inside is now laid out separately.
Spreading Arrays
const fruits = ["apple", "banana", "cherry"];
// Without spread — passes the array as one argument
console.log(fruits); // ["apple", "banana", "cherry"]
// With spread — passes each element as a separate argument
console.log(...fruits); // apple banana cherry
The most practical application: passing array elements as individual function arguments.
const numbers = [5, 1, 8, 3, 9, 2];
// Math.max doesn't accept an array — it wants individual arguments
Math.max(numbers); // NaN ← wrong
Math.max(...numbers); // 9 ← correct
// Same with Math.min
Math.min(...numbers); // 1
Combining Arrays
Spread makes merging arrays clean and expressive:
const first = [1, 2, 3];
const second = [4, 5, 6];
// Old way — concat
const combined = first.concat(second);
// [1, 2, 3, 4, 5, 6]
// Spread — reads exactly as it looks
const combined = [...first, ...second];
// [1, 2, 3, 4, 5, 6]
// Insert in the middle
const withMiddle = [...first, 99, 100, ...second];
// [1, 2, 3, 99, 100, 4, 5, 6]
The spread version communicates intent visually, you can see exactly where each piece goes.
Copying Arrays
A critical use of spread: creating a shallow copy so you don't mutate the original.
const original = [1, 2, 3];
// Without spread — same reference, mutations affect original
const ref = original;
ref.push(4);
console.log(original); // [1, 2, 3, 4] ← mutated!
// With spread — new array, independent copy
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3] ← untouched
console.log(copy); // [1, 2, 3, 4]
This matters enormously in React and other state-driven frameworks where mutating arrays directly causes bugs that are nearly impossible to trace.
Spreading Objects
Spread works on objects too, it copies key-value pairs into a new object:
const defaults = { theme: "light", fontSize: 14, language: "en" };
const userPrefs = { fontSize: 18, language: "hi" };
// Merge: later keys overwrite earlier ones
const settings = { ...defaults, ...userPrefs };
// { theme: "light", fontSize: 18, language: "hi" }
// ↑ from defaults ↑ overwritten ↑ overwritten
The order matters: properties spread later win over earlier ones with the same key.
Adding or Overriding Object Properties
const user = { name: "Priya", role: "viewer", active: true };
// Create updated version without mutating original
const promoted = { ...user, role: "admin" };
// { name: "Priya", role: "admin", active: true }
// Add new properties
const withEmail = { ...user, email: "priya@example.com" };
// { name: "Priya", role: "viewer", active: true, email: "priya@example.com" }
console.log(user); // { name: "Priya", role: "viewer", active: true } ← unchanged
This immutable update pattern, spread the original, then override specific keys, is used constantly in Redux reducers, React state updates, and API response transformations.
Spreading Strings
Strings are iterable, so spread works on them too:
const word = "hello";
const chars = [...word];
// ["h", "e", "l", "l", "o"]
// Useful for string manipulation
const unique = [...new Set("mississippi")];
// ["m", "i", "s", "p"]
The Rest Operator: Collecting Values Inward
Where spread expands, rest collects. It gathers multiple values into a single array. Rest always appears in two specific places: function parameters and destructuring.
Rest in Function Parameters
The rest parameter collects all remaining arguments into an array:
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // 6
sum(10, 20, 30, 40); // 100
sum(5); // 5
numbers inside the function is a real array, you can call reduce, map, filter, and any array method on it.
This replaces the old arguments object, which was array-like but not a real array:
// Old way — arguments object (not a real array)
function oldSum() {
return Array.from(arguments).reduce((a, b) => a + b, 0);
}
// Modern way — rest parameter (real array, no conversion needed)
function newSum(...args) {
return args.reduce((a, b) => a + b, 0);
}
Mixing Named Parameters with Rest
The rest parameter must always be last, it collects everything after the named parameters:
function greetAll(greeting, ...names) {
return names.map(name => `\({greeting}, \){name}!`).join("\n");
}
greetAll("Hello", "Priya", "Arjun", "Zara");
// "Hello, Priya!"
// "Hello, Arjun!"
// "Hello, Zara!"
function logWithLevel(level, timestamp, ...messages) {
messages.forEach(msg => {
console.log(`[\({level}] \){timestamp}: ${msg}`);
});
}
logWithLevel("ERROR", "2025-03-27", "File not found", "Retrying...", "Failed.");
// [ERROR] 2025-03-27: File not found
// [ERROR] 2025-03-27: Retrying...
// [ERROR] 2025-03-27: Failed.
The rest parameter can only appear once and must be at the end, function foo(...a, b) is a syntax error.
Rest in Array Destructuring
Rest in destructuring lets you capture the "everything else" portion of an array:
const [first, second, ...rest] = [10, 20, 30, 40, 50];
console.log(first); // 10
console.log(second); // 20
console.log(rest); // [30, 40, 50]
Skip elements with empty commas:
const [head, , ...tail] = ["a", "b", "c", "d", "e"];
console.log(head); // "a"
console.log(tail); // ["c", "d", "e"]
Practical use, processing a CSV-like array where the first row is headers:
const [headers, ...rows] = [
["Name", "Age", "City"],
["Priya", 28, "Mumbai"],
["Arjun", 32, "Delhi"],
["Zara", 25, "Bangalore"]
];
console.log(headers); // ["Name", "Age", "City"]
console.log(rows); // [["Priya",...], ["Arjun",...], ["Zara",...]]
Rest in Object Destructuring
Extract specific keys and collect everything else into a new object:
const user = {
id: 42,
name: "Priya",
email: "priya@example.com",
role: "admin",
active: true
};
const { id, name, ...profile } = user;
console.log(id); // 42
console.log(name); // "Priya"
console.log(profile); // { email: "priya@example.com", role: "admin", active: true }
This is extremely useful when you need to pass an object to a function but exclude certain sensitive fields:
function sendToClient(userData) {
const { password, internalId, ...safeData } = userData;
return safeData; // password and internalId are stripped
}
Spread vs Rest: The Core Difference
WHERE IT APPEARS WHAT IT DOES
───────────────────── ─────────────────────────────────────
Spread In expressions Expands one iterable → many values
(function calls, [...arr] → val1, val2, val3
array/object literals)
Rest In definitions Collects many values → one array
(function parameters, (val1, val2, val3) → [...args]
destructuring)
A practical rule of thumb: if ... is on the left side of an assignment or in a parameter list, it's rest. If it's on the right side of an assignment or in a function call, it's spread.
// Left side / parameter = REST (collecting)
const [a, ...b] = [1, 2, 3];
function foo(...args) {}
// Right side / call = SPREAD (expanding)
const arr = [...original];
foo(...values);
Practical Use Cases
Use Case 1: Immutable State Updates (React / Redux)
This is likely the highest-frequency real-world use of spread:
// State update — never mutate, always create new
const state = { user: "Priya", count: 0, loading: false };
// Add a property
const withToken = { ...state, token: "abc123" };
// Update a property
const incremented = { ...state, count: state.count + 1 };
// Remove a property (destructuring + rest)
const { loading, ...withoutLoading } = state;
Use Case 2: Flexible Utility Functions
Rest lets you write functions that accept any number of arguments cleanly:
function createTag(tag, className, ...children) {
return `<\({tag} class="\){className}">\({children.join("")}</\){tag}>`;
}
createTag("ul", "list", "<li>Item 1</li>", "<li>Item 2</li>", "<li>Item 3</li>");
// <ul class="list"><li>Item 1</li><li>Item 2</li><li>Item 3</li></ul>
Use Case 3: Merging Configuration Objects
const defaultConfig = {
timeout: 3000,
retries: 3,
headers: { "Content-Type": "application/json" }
};
function makeRequest(url, options = {}) {
const config = { ...defaultConfig, ...options, url };
// options override defaults; url is always set last
return fetch(config.url, config);
}
makeRequest("https://api.example.com/users", { timeout: 5000 });
// config = { timeout: 5000, retries: 3, headers: {...}, url: "..." }
Use Case 4: Cloning and Transforming Arrays in One Step
const scores = [85, 92, 78, 95, 88];
// Add a score and sort — without mutating original
const updated = [...scores, 91].sort((a, b) => b - a);
// [95, 92, 91, 88, 85, 78]
console.log(scores); // [85, 92, 78, 95, 88] ← original unchanged
Use Case 5: Passing Array Contents to Functions That Don't Accept Arrays
const dates = [
new Date("2025-01-15"),
new Date("2025-06-20"),
new Date("2025-03-08")
];
// Math-style — find earliest and latest date
const earliest = new Date(Math.min(...dates));
const latest = new Date(Math.max(...dates));
Use Case 6: De-duplicating an Array
const withDuplicates = [1, 2, 2, 3, 3, 3, 4];
// Spread into a Set (removes duplicates), spread back to array
const unique = [...new Set(withDuplicates)];
// [1, 2, 3, 4]
Things to Watch Out For
Spread creates shallow copies, nested objects are still shared:
const original = { name: "Priya", address: { city: "Mumbai" } };
const copy = { ...original };
copy.name = "Arjun"; // Safe — primitive, independent
copy.address.city = "Delhi"; // Danger — address is still the same reference!
console.log(original.address.city); // "Delhi" ← mutated!
For deep cloning, use structuredClone() (modern) or JSON.parse(JSON.stringify(obj)) (older):
const deepCopy = structuredClone(original);
Rest parameter vs arguments:
// arguments object — available in regular functions, not arrows
function old() { console.log(arguments); } // array-like object
// rest parameter — real array, works in all functions
const modern = (...args) => args.reduce((a, b) => a + b);
Rest must be last:
function foo(a, ...b, c) {} // SyntaxError
function foo(a, b, ...c) {} // Valid
Key Takeaways
Both
...operators use the same syntax but do opposite things: spread expands, rest collects.Spread works in function calls (
fn(...arr)), array literals ([...a, ...b]), and object literals ({...obj}).Rest works in function parameters (
function fn(...args)) and destructuring (const [a, ...b] = arr).Spread creates shallow copies — nested objects remain shared references.
Use spread for immutable updates — a pattern essential in React and Redux.
Rest replaces the old
argumentsobject and gives you a real array with full array method support.The position rule:
...on the left (or in parameters) = rest;...on the right (or in calls) = spread.
Quick Reference
// ── SPREAD ──────────────────────────────────────────────
// Expand into function arguments
Math.max(...[3, 1, 4, 1, 5]); // 5
// Combine arrays
[...arr1, ...arr2]
// Copy array (shallow)
const copy = [...original];
// Copy object (shallow)
const objCopy = { ...original };
// Merge objects (later keys win)
const merged = { ...defaults, ...overrides };
// Override a property immutably
const updated = { ...obj, key: newValue };
// String to char array
[..."hello"] // ["h","e","l","l","o"]
// Deduplicate
[...new Set(arr)]
// ── REST ─────────────────────────────────────────────────
// Collect all arguments
function sum(...args) { return args.reduce((a,b) => a+b, 0); }
// Named params + rest (rest must be last)
function fn(first, second, ...others) {}
// Array destructuring with rest
const [head, ...tail] = [1, 2, 3, 4];
// Object destructuring with rest
const { id, ...rest } = obj;
// Strip sensitive fields
const { password, ...safeUser } = user;
Spread and rest are the same three dots wearing different hats. Once you know which hat they're wearing, every use case clicks into place.

