What you'll learn
Quick Answer
Spread and rest both use three dots, and position decides which you get. Spread expands an iterable into individual elements and is used where values are expected — inside array literals, object literals and function calls. Rest collects remaining items into one array or object and is used where a name is expected — in function parameters and destructuring. Both create shallow copies, so nested objects are still shared with the original.
One Syntax, Two Opposite Jobs
The confusion is understandable: ... means "unpack this" in one place and "gather these" in another. The position tells you which.
Spread expands. It appears where a list of values is expected.
const nums = [1, 2, 3];
console.log(...nums); // 1 2 3 — three separate arguments
const more = [...nums, 4]; // [1, 2, 3, 4]Rest collects. It appears where a variable name is expected — a parameter list, or the left side of a destructuring assignment.
function sum(...values) { // gathers all arguments into an array
return values.reduce((a, b) => a + b, 0);
}
sum(1, 2, 3); // 6
const [first, ...others] = [1, 2, 3]; // first = 1, others = [2, 3]A simple test: if the three dots are on the right of an equals sign or inside a call, it is spread, taking something apart. If they are on the left, or in a parameter list, it is rest, putting things together.
One hard rule for rest: it must be last. function f(...args, last) is a syntax error, because there is no way to know where the gathering should stop.
Copying Arrays and Objects
The most common everyday use is making a copy so you can change it without touching the original.
const original = [3, 1, 2];
const wrong = original;
wrong.sort(); // ALSO sorts original — same array, two names
const right = [...original];
right.sort(); // original untouchedThat matters more than it looks, because several array methods mutate in place — sort, reverse, splice, push. Spreading first is the standard way to avoid surprising the rest of your code.
Objects work the same way, and spreading is also how you update one field immutably:
const user = { name: 'Riya', role: 'user', city: 'Pune' };
const promoted = { ...user, role: 'admin' };
// { name: 'Riya', role: 'admin', city: 'Pune' } — original unchangedOrder matters: later properties overwrite earlier ones. Putting the spread after your override silently undoes it.
const a = { ...user, role: 'admin' }; // role is 'admin'
const b = { role: 'admin', ...user }; // role is 'user' — user winsThis pattern is everywhere in React state updates and Redux reducers, which is why it is worth being fluent in.
The Shallow Copy Trap
This is the part that produces real bugs. Spread copies one level deep. Nested objects and arrays are copied by reference, so they are still shared.
const settings = {
theme: 'dark',
notifications: { email: true, sms: false }
};
const copy = { ...settings };
copy.theme = 'light'; // fine — original unchanged
copy.notifications.email = false; // ALSO changes settings!The top-level keys were copied, but notifications is the same object in both. Changing it through either name changes it for both, which is baffling if you believed you had a copy.
The symptom in React is a component that will not re-render, because the state object looks unchanged by reference even though you mutated something inside it.
For a genuinely deep copy, use structuredClone where available:
const deep = structuredClone(settings); // fully independent
// Nested spread also works when you know the shape
const also = { ...settings, notifications: { ...settings.notifications } };The old JSON.parse(JSON.stringify(obj)) trick works for plain data but silently destroys dates, converting them to strings, and drops functions and undefined values entirely.
Patterns Worth Knowing
Merging. Combine arrays or objects without a loop.
const merged = [...listA, ...listB];
const config = { ...defaults, ...userOptions }; // user wins on conflictsPassing an array as arguments. Replaces the old apply trick.
Math.max(...[3, 7, 2]); // 7
Math.max([3, 7, 2]); // NaN — an array is not three numbersConverting iterables to arrays. Useful with DOM collections, strings, Sets and Maps.
const divs = [...document.querySelectorAll('div')]; // now has array methods
const letters = [...'hello']; // ['h','e','l','l','o']
const unique = [...new Set([1, 1, 2])]; // [1, 2] — dedupe in one lineRemoving a key immutably with rest destructuring:
const { password, ...safeUser } = user; // safeUser has everything except passwordThat last one is genuinely useful in APIs — it is a clean way to strip a sensitive field before sending a response.
Named arguments with defaults combine both forms neatly:
function createUser({ name, role = 'user', ...extra }) {
return { name, role, ...extra };
}
Gotchas Worth Remembering
- Spread only works on iterables in arrays. Spreading a plain object into an array literal throws, because objects are not iterable. Object spread into an object literal is a separate feature and does work.
- Rest parameters are a real array, unlike the old
argumentsobject, somapandfilterwork directly. Arrow functions have noargumentsat all, making rest the only option there. - Rest must come last, in both parameters and destructuring.
- Spreading undefined into an object is safe and produces nothing, but spreading
nullorundefinedinto an array throws. - Getters are evaluated. Spreading an object with a getter calls it and copies the resulting value, not the getter itself.
The mental summary worth keeping: three dots on the right take something apart, three dots on the left put something together, and either way the copy you get is one level deep.
