Spread vs Rest Operators in JavaScript
The Three Dots That Changed Everything ... 🎯

Turning chai into code and ideas into full-stack applications. Sharing lessons from my development journey, one commit at a time.
The Pizza Party Paradox 🍕
Last Friday, my team ordered pizza for a hackathon. We had a problem:
The Situation:
5 different pizza boxes
1 giant serving table
Everyone wanted to grab slices from all boxes without walking around
Solution 1: The Old Way
// Manually take each pizza out
const pizza1 = ["Pepperoni", "Pepperoni", "Pepperoni"];
const pizza2 = ["Veggie", "Veggie"];
const pizza3 = ["Hawaiian", "Hawaiian", "Hawaiian"];
// Create one array by manually adding each
const allSlices = [];
for (let slice of pizza1) allSlices.push(slice);
for (let slice of pizza2) allSlices.push(slice);
for (let slice of pizza3) allSlices.push(slice);
// Tedious!
Solution 2: The Spread Operator
const allSlices = [...pizza1, ...pizza2, ...pizza3];
// Done! One line!
Then someone said: "Let's divide the remaining slices among the three of us."
The Rest Operator:
const [mySlice, yourSlice, ...leftoverSlices] = allSlices;
// mySlice = first slice
// yourSlice = second slice
// leftoverSlices = ALL the rest
That day, I learned that three little dots (...) can do two completely opposite things depending on context:
Spread: Expand/unpack elements (like spreading pizza boxes onto a table)
Rest: Collect/pack elements (like gathering leftovers into one box)
The Three Dots: ... - Same Syntax, Opposite Purposes 🎭
Spread Operator: Expanding Values ➡️
Think: "Unpack this collection and spread the items out"
const box = [1, 2, 3];
// WITHOUT spread
console.log(box); // [1, 2, 3] — one array
// WITH spread
console.log(...box); // 1 2 3 — three separate values
Visual:
Box: [1, 2, 3]
↓ ...spread
Spread: 1 2 3 (individual items)
Rest Operator: Collecting Values
Think: "Collect all the remaining items into one array"
function sum(...numbers) {
// numbers COLLECTS all arguments into an array
}
sum(1, 2, 3, 4, 5);
// Inside the function: numbers = [1, 2, 3, 4, 5]
Visual:
Arguments: 1 2 3 4 5 (individual items)
↓ ...rest
Rest: [1, 2, 3, 4, 5] (one array)
Spread Operator Deep Dive
Case 1: Combining Arrays
const fruits = ["apple", "banana"];
const veggies = ["carrot", "broccoli"];
// Old way: concat
const food1 = fruits.concat(veggies);
// New way: spread
const food2 = [...fruits, ...veggies];
console.log(food2);
// ["apple", "banana", "carrot", "broccoli"]
Why spread is better:
More readable
Can insert items anywhere
Works with multiple arrays
const breakfast = ["eggs", "toast"];
const lunch = ["sandwich"];
const dinner = ["pasta", "salad"];
// Insert in any order!
const meals = [...breakfast, "coffee", ...lunch, "juice", ...dinner];
// ["eggs", "toast", "coffee", "sandwich", "juice", "pasta", "salad"]
Case 2: Copying Arrays (Shallow Copy)
const original = [1, 2, 3];
// Old way: slice
const copy1 = original.slice();
// New way: spread
const copy2 = [...original];
// Modify the copy
copy2.push(4);
console.log(original); // [1, 2, 3] — unchanged
console.log(copy2); // [1, 2, 3, 4] — modified
⚠️ Warning: Shallow Copy Only!
const original = [1, 2, { name: "Alice" }];
const copy = [...original];
// Modifying the nested object affects BOTH
copy[2].name = "Bob";
console.log(original[2].name); // "Bob" 😱
console.log(copy[2].name); // "Bob" 😱
// The object is shared, not copied!
Why? Spread creates a new array, but the references to objects inside are copied, not the objects themselves.
Case 3: Passing Array Elements as Function Arguments
const numbers = [1, 2, 3];
// Old way: apply
Math.max.apply(null, numbers); // 3
// New way: spread
Math.max(...numbers); // 3
How it works:
Math.max(...[1, 2, 3])
↓
Math.max(1, 2, 3) // Spreads into separate arguments
More examples:
const dates = [2024, 0, 15]; // Year, Month (0-indexed), Day
// Create a date with spread
const myDate = new Date(...dates);
console.log(myDate); // Mon Jan 15 2024
// Push multiple items
const arr = [1, 2];
arr.push(...[3, 4, 5]);
console.log(arr); // [1, 2, 3, 4, 5]
Case 4: Spreading Objects (ES2018+)
const user = { name: "Alice", age: 25 };
const location = { city: "NYC", country: "USA" };
// Combine objects
const profile = { ...user, ...location };
console.log(profile);
// { name: "Alice", age: 25, city: "NYC", country: "USA" }
Overriding properties:
const defaults = { theme: "dark", fontSize: 14 };
const userSettings = { fontSize: 16 }; // Override fontSize
const settings = { ...defaults, ...userSettings };
console.log(settings);
// { theme: "dark", fontSize: 16 } — userSettings wins!
Order matters!
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
// obj2 comes second, its 'b' wins
const result1 = { ...obj1, ...obj2 };
console.log(result1); // { a: 1, b: 3, c: 4 }
// obj1 comes second, its 'b' wins
const result2 = { ...obj2, ...obj1 };
console.log(result2); // { a: 1, b: 2, c: 4 }
Case 5: Adding Properties to Objects
const user = { name: "Bob", age: 30 };
// Add a new property
const userWithId = { ...user, id: 123 };
console.log(userWithId);
// { name: "Bob", age: 30, id: 123 }
// Override existing + add new
const updated = { ...user, age: 31, verified: true };
console.log(updated);
// { name: "Bob", age: 31, verified: true }
Rest Operator Deep Dive
Case 1: Function Parameters (Variadic Functions)
// Old way: arguments object
function oldSum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
// New way: rest parameters
function sum(...numbers) {
return numbers.reduce((acc, num) => acc + num, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum()); // 0
Why rest is better:
numbersis a real array (has.map(),.filter(), etc.)argumentsis array-like but NOT an arrayMore readable
Case 2: Collecting Remaining Arguments
function introduce(greeting, ...names) {
console.log(greeting);
console.log("People:", names);
}
introduce("Hello", "Alice", "Bob", "Charlie");
// Output:
// Hello
// People: ["Alice", "Bob", "Charlie"]
Rule: Rest parameter MUST be last!
// CORRECT
function func(a, b, ...rest) { }
// WRONG: Rest must be last
function func(...rest, a, b) { } // SyntaxError
Case 3: Array Destructuring
const numbers = [1, 2, 3, 4, 5];
// Get first two, collect the rest
const [first, second, ...rest] = numbers;
console.log(first); // 1
console.log(second); // 2
console.log(rest); // [3, 4, 5]
Real-world example:
const [winner, runnerUp, ...participants] = [
"Alice",
"Bob",
"Charlie",
"Diana",
"Eve"
];
console.log(winner); // "Alice"
console.log(runnerUp); // "Bob"
console.log(participants); // ["Charlie", "Diana", "Eve"]
Case 4: Object Destructuring
const user = {
id: 1,
name: "Alice",
age: 25,
city: "NYC",
country: "USA"
};
// Extract specific properties, collect the rest
const { id, name, ...otherDetails } = user;
console.log(id); // 1
console.log(name); // "Alice"
console.log(otherDetails); // { age: 25, city: "NYC", country: "USA" }
Practical use: Removing properties
const user = { id: 1, name: "Alice", password: "secret123" };
// Remove password, keep everything else
const { password, ...safeUser } = user;
console.log(safeUser); // { id: 1, name: "Alice" }
// Password is excluded!
Spread vs Rest: The Key Differences
| Aspect | Spread ... |
Rest ... |
|---|---|---|
| Direction | Expands/unpacks | Collects/packs |
| Use in | Array literals, object literals, function calls | Function parameters, destructuring |
| Result | Individual elements | Array (or object) |
| Example | Math.max(...arr) |
function sum(...nums) |
| Think of it as | Spreading items out | Gathering items up |
Same syntax, opposite jobs!
Real-World Use Cases
Example 1: Merging State in React
// React component state update
const [state, setState] = useState({
user: "Alice",
theme: "dark",
notifications: true
});
// Update just the theme, keep everything else
setState({
...state,
theme: "light"
});
// Result: { user: "Alice", theme: "light", notifications: true }
Example 2: Building Flexible APIs
function createUser(name, email, ...permissions) {
return {
name,
email,
permissions,
createdAt: new Date()
};
}
const admin = createUser("Alice", "alice@example.com", "read", "write", "delete");
console.log(admin);
// {
// name: "Alice",
// email: "alice@example.com",
// permissions: ["read", "write", "delete"],
// createdAt: 2026-04-12T...
// }
const guest = createUser("Bob", "bob@example.com", "read");
console.log(guest);
// {
// name: "Bob",
// email: "bob@example.com",
// permissions: ["read"],
// createdAt: 2026-04-12T...
// }
Example 3: Cloning and Updating Nested Data
const cart = {
items: [
{ id: 1, name: "Laptop", price: 999 },
{ id: 2, name: "Mouse", price: 25 }
],
total: 1024
};
// Add a new item without mutating original
const updatedCart = {
...cart,
items: [...cart.items, { id: 3, name: "Keyboard", price: 75 }],
total: cart.total + 75
};
console.log(cart.items.length); // 2 — original unchanged
console.log(updatedCart.items.length); // 3 — new cart has the item
Example 4: Flexible Logger Function
function log(level, ...messages) {
const timestamp = new Date().toISOString();
console.log(`[\({timestamp}] [\){level}]`, ...messages);
}
log("INFO", "Server started");
// [2026-04-12T...] [INFO] Server started
log("ERROR", "Database connection failed:", "Timeout after 30s");
// [2026-04-12T...] [ERROR] Database connection failed: Timeout after 30s
Example 5: Removing Duplicates from Arrays
const numbers = [1, 2, 2, 3, 4, 4, 5];
// Combine Set (removes duplicates) with spread
const unique = [...new Set(numbers)];
console.log(unique); // [1, 2, 3, 4, 5]
How it works:
new Set(numbers)— Creates a Set (no duplicates)...— Spreads the Set back into an array
Common Mistakes and Gotchas
Mistake 1: Shallow Copy Confusion
const original = [[1, 2], [3, 4]];
const copy = [...original];
// Modifying nested array affects BOTH
copy[0].push(99);
console.log(original[0]); // [1, 2, 99] 😱
console.log(copy[0]); // [1, 2, 99] 😱
Solution: Deep Copy
const deepCopy = JSON.parse(JSON.stringify(original));
// Or use lodash's _.cloneDeep()
Mistake 2: Using Rest in the Middle
// WRONG
function func(...rest, last) { }
// SyntaxError: Rest parameter must be last
// CORRECT
function func(first, ...rest) { }
Mistake 3: Spreading Non-Iterables
const num = 123;
// WRONG
const arr = [...num]; // TypeError: num is not iterable
// CORRECT: Only spread iterables (arrays, strings, Sets, Maps)
const str = "hello";
const chars = [...str]; // ["h", "e", "l", "l", "o"]
Mistake 4: Expecting Deep Merge
const obj1 = { a: { b: 1 } };
const obj2 = { a: { c: 2 } };
const merged = { ...obj1, ...obj2 };
console.log(merged);
// { a: { c: 2 } } 😱
// Expected: { a: { b: 1, c: 2 } }
// Reality: obj2.a completely REPLACES obj1.a
Why? Spread does shallow merge, not deep merge.
Solution:
const merged = {
...obj1,
a: { ...obj1.a, ...obj2.a }
};
console.log(merged);
// { a: { b: 1, c: 2 } }
Interview Questions
Q1: What's the difference between spread and rest?
Strong Answer: "Both use the same ... syntax, but in opposite ways. Spread expands an array or object into individual elements—like when you spread an array into function arguments. Rest does the reverse: it collects multiple elements into a single array—like when you gather all remaining function arguments into one parameter. The key is context: spread is used where values are expected (function calls, array literals), while rest is used where variable names are expected (function parameters, destructuring)."
Q2: Can you use spread with objects?
Strong Answer: "Yes, starting with ES2018, spread works with objects too. It creates a shallow copy and merges properties. Later properties override earlier ones. For example, { ...obj1, ...obj2 } creates a new object with all properties from both, with obj2's properties taking precedence if there are conflicts."
Q3: Is spread/rest a shallow or deep copy?
Strong Answer: "Both create shallow copies. For arrays and objects, only the first level is copied. Nested objects or arrays are still referenced from the original. If you need a deep copy, you'll need to either manually copy each level or use utilities like JSON.parse(JSON.stringify()) for simple cases, or lodash's cloneDeep() for more complex structures."
Practice Challenges 🏋️
Challenge 1: Merge and Deduplicate Arrays
const arr1 = [1, 2, 3];
const arr2 = [3, 4, 5];
const arr3 = [5, 6, 7];
// Merge all arrays and remove duplicates
Solution:
const merged = [...arr1, ...arr2, ...arr3];
const unique = [...new Set(merged)];
console.log(unique); // [1, 2, 3, 4, 5, 6, 7]
// One-liner:
const result = [...new Set([...arr1, ...arr2, ...arr3])];
Challenge 2: Create a Flexible Sum Function
// Should work with any number of arguments
sum(1, 2); // 3
sum(1, 2, 3, 4); // 10
sum(); // 0
Solution:
function sum(...numbers) {
return numbers.reduce((acc, num) => acc + num, 0);
}
Challenge 3: Update Nested Object Immutably
const user = {
id: 1,
profile: {
name: "Alice",
age: 25
}
};
// Update user.profile.age to 26 WITHOUT mutating original
Solution:
const updatedUser = {
...user,
profile: {
...user.profile,
age: 26
}
};
console.log(user.profile.age); // 25 — original unchanged
console.log(updatedUser.profile.age); // 26 — updated
The Pizza Party Ending 🍕
Back at the hackathon, after devouring all the pizza, someone asked: "How did we combine all those boxes so easily?"
I smiled. "The spread operator. It's like magic—takes multiple arrays and expands them into one."
"And what about dividing the leftovers?"
"The rest operator. It's the opposite magic—takes multiple items and collects them into one array."
My teammate laughed. "So it's the same three dots, but sometimes it spreads things out, and sometimes it gathers them up?"
"Exactly! Context is everything. That's JavaScript for you—same syntax, opposite behaviors."
Three dots. Two operators. Infinite possibilities.
Key Takeaways
Spread expands (
...arr→ individual items)Rest collects (
...rest→ array)Same syntax, different contexts
Spread: function calls, array literals, object literals
Rest: function parameters, destructuring
Both create shallow copies — nested objects are referenced
Spread can merge arrays and objects
Rest enables flexible function parameters