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 untouched

That 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 unchanged

Order 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 wins

This 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 conflicts

Passing 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 numbers

Converting 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 line

Removing a key immutably with rest destructuring:

const { password, ...safeUser } = user;   // safeUser has everything except password

That 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 arguments object, so map and filter work directly. Arrow functions have no arguments at 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 null or undefined into 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.

Frequently Asked Questions

What is the difference between spread and rest? They share syntax but do opposite things. Spread expands an iterable into individual values and appears where values are expected. Rest collects remaining values into an array or object and appears where a name is expected, such as a parameter list.
Does spread create a deep copy? No, only a shallow one. Top-level values are copied, but nested objects and arrays are shared with the original, so mutating them affects both. Use structuredClone for a genuine deep copy.
Why does Math.max not work with an array? Math.max expects separate numeric arguments, not one array. Spreading the array with Math.max(...nums) passes each element as its own argument. Passing the array directly gives NaN.
Can I use rest parameters in arrow functions? Yes, and you must — arrow functions do not have their own arguments object, so rest parameters are the only way to collect a variable number of arguments there.
How do I remove a property from an object immutably? Use rest destructuring: const { password, ...rest } = user gives you rest containing everything except password, without modifying the original object. It is a common way to strip sensitive fields before returning data.