There’s a small piece of modern JavaScript syntax you’ll bump into constantly once you start reading real code: three dots, .... It shows up when copying an array, merging two objects, passing a list of arguments, or collecting “everything else” into a single variable. The confusing part is that those same three dots do two opposite things depending on where you write them. In one place they take a collection apart; in another they pull loose values together. This article untangles that for good.
The two behaviours have names. When ... expands something into its individual pieces, it’s the spread operator. When it gathers several values into one array, it’s a rest element. Same symbol, mirror-image jobs. We’ll take them one at a time, with plenty of real examples, and by the end you’ll always know which one you’re looking at.
If you’re still getting comfortable with the data structures themselves, it’s worth having read arrays and objects and functions first — everything here builds directly on both.
What the three dots actually do
Before the names, hold on to one simple idea. The ... syntax always sits next to a collection or a variable, and its direction depends on context:
- On the right side of an assignment, or inside a
[ ]/{ }/ function call, it spreads — it opens a collection up and lays its items out individually. - On the left side, in a function parameter list or a destructuring pattern, it rests — it scoops up the leftover values into a fresh array.
That’s the whole trick. “Am I building something out of pieces, or catching pieces into a container?” Answer that and you know whether you’re spreading or resting. Let’s see both in action.
The spread operator: unpacking a collection
The spread operator takes an iterable — most often an array — and spreads its elements out as if you’d typed them one by one. The simplest place to feel it is array building:
const numbers = [1, 2, 3];
const more = [...numbers, 4, 5];
console.log(more); // [1, 2, 3, 4, 5]
...numbers doesn’t put the array inside the new array — it pours its contents in. So more becomes [1, 2, 3, 4, 5], not [[1, 2, 3], 4, 5]. That distinction is the heart of spread: it unwraps one level.
Copying an array
One of the most common uses is making a shallow copy of an array. Because spread lays out fresh elements into a brand-new array, the copy is independent of the original:
const original = ["a", "b", "c"];
const copy = [...original];
copy.push("d");
console.log(original); // ["a", "b", "c"] (untouched)
console.log(copy); // ["a", "b", "c", "d"]
This matters because const copy = original would not copy anything — both names would point at the same array, and pushing to one would change the other. Spread gives you a genuinely separate array.
Merging arrays
Need to glue several arrays together? Spread each one inside a new array literal:
const front = [1, 2];
const back = [3, 4];
const combined = [...front, ...back];
console.log(combined); // [1, 2, 3, 4]
// you can sprinkle in extra values anywhere
const padded = [0, ...front, 99, ...back];
console.log(padded); // [0, 1, 2, 99, 3, 4]
This reads cleanly and you control the order precisely — far nicer than older approaches with concat.
Spreading objects
Spread works on objects too (since ES2018), copying their key-value pairs into a new object. This is the modern way to clone or extend an object:
const user = { name: "John Doe", role: "editor" };
const updated = { ...user, role: "admin" };
console.log(updated); // { name: "John Doe", role: "admin" }
console.log(user); // { name: "John Doe", role: "editor" } (untouched)
Here ...user copies every property of user into the new object, and then role: "admin" is applied afterward. Order matters: a key written later wins, so updated keeps the name but overrides the role. The original user is left exactly as it was — this is the standard pattern for “give me a copy of this object with one field changed,” which you’ll use everywhere from React state to API payloads.
Spread copies are shallow
Spread duplicates only the top level. If a value inside the array or object is itself an array or object, the copy still shares that same nested reference with the original.
const a = { name: "John Doe", tags: ["x", "y"] };
const b = { ...a };
b.tags.push("z");
console.log(a.tags); // ["x", "y", "z"] ← the original changed too!b got its own top-level object, but b.tags and a.tags are still the one and the same array. For a fully independent deep copy you need a deliberate deep clone (for example structuredClone(a) in modern environments). For flat data, shallow is exactly what you want — just stay aware of the boundary.
Spreading into a function call
Spread also works where you call a function, turning an array into a list of separate arguments. This is perfect for functions that expect individual values, like Math.max:
const scores = [82, 96, 71, 88];
console.log(Math.max(...scores)); // 96
// equivalent to Math.max(82, 96, 71, 88)
Math.max doesn’t accept an array — it wants arguments one after another. Spreading the array hands each element over as its own argument. Before this syntax existed, people reached for Math.max.apply(null, scores); spread does the same thing far more readably.
Rest: gathering values into an array
Now flip the direction. Rest appears on the receiving side and does the opposite of spread: instead of opening a collection up, it collects loose values into a single array. You’ll meet it in two places — function parameters and destructuring.
Rest parameters in functions
Say you want a function that adds up however many numbers it’s given — two, five, twenty, you don’t know in advance. A rest parameter catches all the arguments into one real array:
function sum(...nums) {
let total = 0;
for (const n of nums) {
total += n;
}
return total;
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(10, 20, 30, 40)); // 100
console.log(sum()); // 0
Inside sum, nums is an ordinary array containing every argument passed in, so you can loop over it, call .length, use array methods — anything. The ... here gathers, which is exactly the reverse of the spread call we just saw.
You can also have fixed parameters first, and let rest mop up the remainder. The rest parameter must be last:
function greet(greeting, ...names) {
return names.map((n) => `${greeting}, ${n}!`);
}
console.log(greet("Hi", "John Doe", "Jane Doe"));
// ["Hi, John Doe!", "Hi, Jane Doe!"]
greeting grabs the first argument; ...names collects all the rest into an array. Clean and explicit.
Rest parameters beat the old arguments object
Older JavaScript exposed a special arguments object inside every function to reach extra arguments. Rest parameters are better in every way that matters: ...nums is a real array (so .map, .filter, .reduce all work directly), arguments is only an array-like object that you’d have to convert first. Rest also works inside arrow functions, where arguments doesn’t exist at all. In new code, prefer rest parameters and forget arguments exists.
Rest in destructuring
The same idea works when you pull values out of an array or object. Rest grabs “everything I didn’t name individually.” With arrays:
const [first, second, ...others] = [10, 20, 30, 40, 50];
console.log(first); // 10
console.log(second); // 20
console.log(others); // [30, 40, 50]
first and second take the first two items; ...others collects the leftover items into a new array. This is a tidy way to split “the head” from “the tail” of a list.
It works with objects too, where rest gathers the remaining properties into a new object:
const person = { name: "Jane Doe", age: 31, city: "Jakarta", role: "admin" };
const { name, ...rest } = person;
console.log(name); // "Jane Doe"
console.log(rest); // { age: 31, city: "Jakarta", role: "admin" }
A practical use: this is the cleanest way to remove a property from an object without mutating the original. Pull off the key you want gone, and rest holds everything else.
// drop the password before sending a user object to the client
const { password, ...safeUser } = fullUser;
// safeUser has every field except password
Spread vs rest: telling them apart
Same three dots, so how do you instantly know which one you’re reading? Look at which side of the action it’s on:
- Spread is on the value/output side. It’s inside something you’re building: a
[ ]array literal, a{ }object literal, or a function call’s arguments. It takes a collection apart. - Rest is on the binding side. It’s where values are received: a function’s parameter list, or the left-hand side of a destructuring assignment. It packs values up.
Read it as a question: Is this expanding a collection into pieces (spread), or catching pieces into a collection (rest)? The position answers it every time.
// SPREAD — unpacking, on the building side
const all = [...a, ...b]; // inside an array literal
fn(...args); // inside a function call
// REST — gathering, on the receiving side
function fn(...args) { } // in a parameter list
const [head, ...tail] = list; // in a destructuring pattern
One symbol, two roles — that's normal
It can feel odd that one operator means two opposite things. But notice they never collide: spread only makes sense where you’re constructing a value, and rest only makes sense where you’re receiving one. The context they live in never overlaps, so JavaScript (and you) can always tell which is meant. Lean on the position rule and the ambiguity disappears.
A few real patterns you’ll reach for
These small idioms come up again and again, so they’re worth recognising on sight:
// 1. Clone-and-update an object (immutability-friendly)
const nextState = { ...state, loading: false };
// 2. Add an item to an array without mutating the original
const withNew = [...items, newItem];
// 3. Turn any iterable into a real array (string → chars, Set → array)
const chars = [...("hello")]; // ["h", "e", "l", "l", "o"]
const unique = [...new Set([1, 1, 2, 3, 3])]; // [1, 2, 3]
// 4. Give a function default-style flexible arguments
function logAll(label, ...messages) {
messages.forEach((m) => console.log(label, m));
}
That third one is a neat trick: spreading a Set is the shortest way to deduplicate an array, because a Set drops duplicates and spread turns it back into a plain array.
Wrapping up
Three dots, two opposite jobs — and now you can always tell them apart:
- Spread (
...) expands a collection into individual pieces. Use it inside array literals, object literals, and function calls to copy, merge, and unpack:[...arr],{ ...obj },fn(...args),Math.max(...nums). - Rest (
...) gathers loose values into a single array. Use it in function parameters (function f(...args)) and in destructuring (const [first, ...others] = list) to collect “everything else.” - The position tells you which is which: building a value → spread; receiving values → rest.
- Spread copies are shallow — nested arrays/objects stay shared with the original. Reach for
structuredClonewhen you truly need a deep copy. - Spread is the modern way to do immutable updates: copy-then-change instead of mutating in place.
Spread and rest are everywhere in modern JavaScript and the frameworks built on it, so getting fluent here pays off immediately. They pair naturally with destructuring — pulling values out of arrays and objects by shape — which is the perfect thing to master alongside them.