Spread vs Rest in JavaScript: The Same Dots, Two Different Jobs
If you have been writing JavaScript for a while, you have probably seen those three dots ... everywhere. In one place they expand an array into pieces. In another place they scoop up leftover arguments into a bundle. Same syntax, completely opposite behavior.
That is the trick. The dots do not have a fixed personality. They change based on where you put them. Think of them like a kitchen tool. A whisk can mix eggs into a smooth batter, or it can whip air into cream. Same tool, different job depending on context.
Let us look at both jobs in detail.
The Spread Operator: Unpacking Things
Spread takes something that holds multiple values and spreads those values out, one by one, into wherever you need them.
Arrays: Making Copies and Combinations
The most common use is copying an array without mutating the original.
const fruits = ['apple', 'banana', 'orange'];
const fruitsCopy = [...fruits];
console.log(fruitsCopy);
// ['apple', 'banana', 'orange']
console.log(fruitsCopy === fruits);
// false (different array in memory)
This is not the same as const copy = fruits. That just creates another name pointing to the same basket. Spread creates a brand new basket with the same contents.
You can also merge arrays cleanly.
const frontEnd = ['React', 'Vue'];
const backEnd = ['Node', 'Django'];
const fullStack = [...frontEnd, ...backEnd];
console.log(fullStack);
// ['React', 'Vue', 'Node', 'Django']
No concat, no loops, no mess. You can even slip new items in between.
const withExtras = ['HTML', ...frontEnd, 'CSS', ...backEnd];
console.log(withExtras);
// ['HTML', 'React', 'Vue', 'CSS', 'Node', 'Django']
Objects: Shallow Copies and Overriding Properties
Spread works with objects too. You can clone an object or merge properties from multiple sources.
const user = { name: 'Ravi', role: 'developer' };
const details = { city: 'Bangalore', experience: 3 };
const profile = { ...user, ...details };
console.log(profile);
// { name: 'Ravi', role: 'developer', city: 'Bangalore', experience: 3 }
Here is where it gets useful. You can override specific properties while keeping everything else.
const updatedUser = { ...user, role: 'senior developer' };
console.log(updatedUser);
// { name: 'Ravi', role: 'senior developer' }
The order matters. If user and details both have a role property, the last one wins.
const a = { x: 1, y: 2 };
const b = { y: 99, z: 3 };
const merged = { ...a, ...b };
console.log(merged);
// { x: 1, y: 99, z: 3 }
y becomes 99 because b came last and overwrote it.
Passing Arrays as Individual Arguments
Before spread, if you had an array of numbers and wanted to find the maximum, you would do this awkward dance:
const numbers = [10, 5, 30, 2];
// Old way
const max = Math.max.apply(null, numbers);
Spread makes it readable.
const max = Math.max(...numbers);
console.log(max); // 30
Math.max does not accept an array. It wants individual numbers. Spread unpacks the array and hands them over one by one.
// What actually happens under the hood
Math.max(10, 5, 30, 2);
A Quick Warning About Shallow Copies
Spread only copies the top level. If your array contains objects, or your object contains nested objects, those inner references stay shared.
const team = [
{ name: 'Alice', skills: ['JS'] },
{ name: 'Bob', skills: ['Python'] }
];
const teamCopy = [...team];
teamCopy[0].skills.push('React');
console.log(team[0].skills);
// ['JS', 'React'] (original changed too!)
Both team and teamCopy point to the same inner objects. If you need a deep copy, spread alone is not enough. You would need something like structuredClone or a library.
The Rest Operator: Gathering Leftovers
If spread is about unpacking a suitcase, rest is about throwing loose items into a bag. It collects individual values into a single array.
In Function Parameters
The classic use case is when a function needs to accept any number of arguments.
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20)); // 30
console.log(sum()); // 0
Inside sum, numbers is a real array. You can call .map, .filter, .reduce on it directly. This is a huge upgrade from the old arguments object, which looked like an array but was not one.
// Old way (don't do this)
function oldSum() {
// arguments is not a real array
return Array.from(arguments).reduce((a, b) => a + b, 0);
}
Rest also works when you want to separate some named arguments from the leftovers.
function createUser(name, email, ...preferences) {
return {
name,
email,
preferences // an array of whatever else was passed
};
}
const user = createUser('Ravi', 'ravi@example.com', 'darkMode', 'notifications', 'weeklyDigest');
console.log(user);
// {
// name: 'Ravi',
// email: 'ravi@example.com',
// preferences: ['darkMode', 'notifications', 'weeklyDigest']
// }
The first two arguments get proper names. Everything else gets bundled into preferences. This is clean and self-documenting.
In Destructuring
Rest shines when you are pulling values out of an array or object and want to keep whatever is left.
With arrays:
const colors = ['red', 'green', 'blue', 'yellow', 'purple'];
const [primary, secondary, ...others] = colors;
console.log(primary); // 'red'
console.log(secondary); // 'green'
console.log(others); // ['blue', 'yellow', 'purple']
With objects:
const product = {
id: 101,
name: 'Wireless Mouse',
price: 29.99,
stock: 150,
category: 'Electronics'
};
const { id, name, ...metadata } = product;
console.log(id); // 101
console.log(name); // 'Wireless Mouse'
console.log(metadata);
// { price: 29.99, stock: 150, category: 'Electronics' }
This pattern is incredibly useful when you want to remove specific properties before sending data to an API or a child component.
function sendToApi(product) {
// Strip out internal fields, keep only what the API needs
const { internalId, createdBy, ...apiPayload } = product;
return fetch('/api/products', { body: JSON.stringify(apiPayload) });
}
The Visual Difference
Here is the simplest way to keep them straight in your head:
Spread takes one container and breaks it into pieces. Rest takes many pieces and builds one container.
Key Differences at a Glance
| Aspect | Spread (...) |
Rest (...) |
|---|---|---|
| Direction | Expands out | Collects in |
| Where it lives | On the right side of =, in function calls, in array/object literals |
On the left side of =, in function parameters, in destructuring |
| Input | One iterable (array, string, object) | Multiple individual values |
| Output | Individual values | A single array |
| Must be last | No | Yes, when destructuring |
Practical Use Cases You Will Actually Use
1. Cloning and Updating State in React
const [user, setUser] = useState({ name: 'Ravi', age: 28 });
// Update just the age
setUser({ ...user, age: 29 });
2. Combining Configuration Objects
const defaults = { theme: 'light', notifications: true };
const userSettings = { theme: 'dark' };
const finalConfig = { ...defaults, ...userSettings };
// { theme: 'dark', notifications: true }
User settings override defaults without destroying untouched defaults.
3. Logging with Rest
function log(level, ...messages) {
console[level](`[${new Date().toISOString()}]`, ...messages);
}
log('warn', 'Disk space low', 'Cleanup recommended');
// [2024-01-15T10:30:00.000Z] Disk space low Cleanup recommended
4. Removing Properties Without Mutating
function removePassword(user) {
const { password, ...safeUser } = user;
return safeUser;
}
5. Converting a NodeList to an Array
const buttons = document.querySelectorAll('button');
const buttonArray = [...buttons];
buttonArray.map(btn => btn.addEventListener('click', handleClick));
querySelectorAll returns a NodeList, not an array. Spread converts it instantly.
The One Rule to Remember
If the three dots are on the receiving end, collecting values into a variable, it is rest. If they are on the giving end, unpacking values into a new place, it is spread.
// Rest: collecting
const [first, ...rest] = array;
// Spread: expanding
const newArray = [...array];
That is it. Same three dots. Opposite jobs. Context tells you which one is working.