Spread vs Rest Operators in JavaScript

Introduction
JavaScript's Spread & Rest Operator look identical. Both are three dots (...). But they do opposite things depending on where you put them.
One expands values out. The other collects values in. Once that clicks, the rest is just syntax.
What the Spread Operator Does
The spread operator takes something iterable, like an array or object, and unpacks its contents into individual elements.
Think of it like opening a box and laying everything out on the table.
const fruits = ['apple', 'banana', 'mango'];
console.log(...fruits); // apple banana mango
That's useful, but the real power shows up when you're building new arrays or objects.
const first = [1, 2, 3];
const second = [4, 5, 6];
const combined = [...first, ...second];
// [1, 2, 3, 4, 5, 6]
No concat. No loops. You just spread both arrays into a new one.
What the Rest Operator Does
Rest does the reverse. Instead of expanding values, it collects multiple values into a single array.
It's most useful in function parameters when you don't know how many arguments are coming in.
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
sum(1, 2, 3); // 6
sum(10, 20, 30, 40); // 100
Here, ...numbers gathers every argument into an array. You're not unpacking anything. You're bundling things up.
The Core Difference
| Spread | Rest | |
|---|---|---|
| What it does | Expands an iterable into individual values | Collects individual values into an array |
| Where it's used | Array/object literals, function calls | Function parameter lists |
| Direction | Out (unpacking) | In (bundling) |
A quick way to remember: if the ... is on the right side of an assignment or inside a function call, it's spread. If it's in the function definition's parameter list, it's rest.
Using Spread with Arrays
Copying an array
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3]
console.log(copy); // [1, 2, 3, 4]
This gives you a shallow copy. Mutating copy won't touch original. That said, if the array contains objects, those objects are still shared by reference.
Inserting values while merging
const start = [1, 2];
const end = [5, 6];
const full = [...start, 3, 4, ...end];
// [1, 2, 3, 4, 5, 6]
You can drop values in between spreads. That's something concat can't do as cleanly.
Passing array items as function arguments
const nums = [3, 1, 4, 1, 5];
const max = Math.max(...nums); // 5
Math.max expects individual arguments, not an array. Spread bridges that gap without needing .apply().
Using Spread with Objects
Spread works on objects too. It copies key-value pairs into a new object.
const defaults = { theme: 'light', lang: 'en' };
const userPrefs = { lang: 'fr', fontSize: 14 };
const settings = { ...defaults, ...userPrefs };
// { theme: 'light', lang: 'fr', fontSize: 14 }
When keys overlap, the last one wins. Here, lang from userPrefs overwrites the one from defaults.
This pattern shows up constantly in config merging, Redux reducers, and anywhere you're building objects from multiple sources.
Practical Use Cases
Removing a property from an object
const user = { id: 1, name: 'Sara', password: 'secret' };
const { password, ...safeUser } = user;
console.log(safeUser); // { id: 1, name: 'Sara' }
Here rest is used in destructuring. You pull out the property you don't want, and everything else goes into safeUser.
Handling variable function arguments
function log(level, ...messages) {
messages.forEach(msg => console.log(`[${level}]`, msg));
}
log('INFO', 'Server started', 'Listening on port 3000');
// [INFO] Server started
// [INFO] Listening on port 3000
The first argument is captured separately. Everything after goes into the rest array.
Adding or overriding properties without mutation
const state = { count: 0, loading: false };
const nextState = { ...state, count: state.count + 1 };
// { count: 1, loading: false }
You get a new object. The original is untouched. This is the standard way to handle state updates in React without mutating directly.
Cloning and extending arrays
const base = ['read', 'write'];
const extended = [...base, 'execute'];
// ['read', 'write', 'execute']
Useful when building permission sets or option lists that inherit from a base.
A Note on Shallow Copies
Both spread and rest create shallow copies. For arrays and objects of primitives, that's fine. But if your data is nested, you'll need something deeper, like structuredClone, for full isolation.
const nested = { a: { b: 1 } };
const copy = { ...nested };
copy.a.b = 99;
console.log(nested.a.b); // 99 (shared reference)
This trips people up. When you need a fully independent copy, use structuredClone. It recursively clones the entire structure, so nothing is shared.
const nested = { a: { b: 1 } };
const clone = structuredClone(nested);
clone.a.b = 99;
console.log(nested.a.b); // 1 (untouched)
structuredClone is built into modern browsers and Node.js 17+, so no library needed. For deeply nested data, it's the cleaner option over spread.
Conclusion
The same three dots, two different jobs. Spread unpacks. Rest collects. Once you see that pattern, you'll start noticing it everywhere, and using it without thinking twice.




