Spread & rest
Learn how JavaScript’s three dots expand arrays, calls, and objects, collect leftover values with rest, and make safe shallow copies and immutable updates.
- 01Read the direction of
...Tell when the dots expand values and when they collect leftovers. - 02Merge with confidencePredict array and object spread order, including which object property wins.
- 03Copy without surprisesUse shallow copies and immutable updates, and know when
structuredCloneis safer.
Same dots, two directions
JavaScript uses ... for two opposite jobs. The name depends on where the dots appear: spread expands, and rest collects. You already met rest parameters and spread in calls in the Default & rest parameters lesson. Here we go deeper: arrays, calls, objects, destructuring, merge order, shallow copies, and the edge cases professionals trip over.
Spread expands one value into many positions; rest collects many leftover positions into one value.
Imagine a pouch of marbles. Spread is pouring the pouch out so every marble stands alone on the table. Rest is sweeping the leftover marbles back into one pouch. Same three dots, opposite direction.
- In real life: Pour a bag onto the table
- In JavaScript: Spread:
...bagexpands each item - In real life: Each marble now stands alone
- In JavaScript: Array elements, call arguments, or object properties appear separately
- In real life: Sweep leftover marbles back into one bag
- In JavaScript: Rest:
...leftoverscollects what was not already taken
Where the analogy stops: The marbles do not have keys, prototypes, getters, or references. JavaScript values do, so the rest of the lesson adds those rules.
Where ... appears | Name | What happens | Tiny example |
|---|---|---|---|
| Array literal | Spread | Expands an iterable into elements | [0, ...items] |
| Function call | Spread | Expands an iterable into arguments | fn(...args) |
| Object literal | Spread | Copies own enumerable properties | { ...a, ...b } |
| Array destructuring | Rest | Collects remaining elements | const [x, ...xs] = list |
| Object destructuring | Rest | Collects remaining own enumerable properties | const { id, ...rest } = obj |
| Function parameters | Rest | Collects remaining arguments | function f(...args) {} |
Try the first lab before reading further. Each card contains .... Your job is to decide whether those dots expand or collect.
const all = [0, ...numbers];Math.max(...scores)const merged = { ...defaults, ...user };const [first, ...others] = list;const { password, ...safeUser } = user;function sum(...numbers) { }
Sort by direction: are the dots expanding a value, or collecting leftovers?
Spread in arrays & calls
STEP THROUGHIn an array literal, spread inserts the elements of an iterable at that exact position. It is a non-mutating way to copy, prepend, append, or insert. In a function call, spread turns an iterable into separate arguments.
Array literal
["intro", ...front, "quiz"] creates a new array. The old front array is not changed.
Function call
Math.max(...scores) calls Math.max with each score as its own argument, not with one array argument.
Step through the example. Then switch line 3 and notice that spread obeys order exactly: it does not sort, flatten deeply, or mutate the source arrays.
Predict the order in lesson, then step. Switch the setting and notice that spread keeps the source arrays but changes where their elements land.
script
const back = ["quiz"];const lesson = ["intro", ...front, "lab", ...back];console.log(lesson.join(" -> "));function announce(first, second, third) { console.log(first + " | " + second + " | " + third);}announce(...lesson.slice(0, 3));The Slicing & splicing lesson introduced concat and spread for array copying. Use spread when the position is easy to read at a glance, especially for small immutable updates.
Spread in objects
INTERACTIVEIn an object literal, spread copies own enumerable properties into a new object. The copy happens from left to right. If two sources name the same property, the later one overwrites the earlier one.
Think of each object as a shopping list. Start with defaults, then add the user's list. If both lists name theme, the newer note is the one JavaScript keeps.
- In real life: The first shopping list
- In JavaScript:
defaultscopied first - In real life: A newer shopping list
- In JavaScript:
userSettingscopied later - In real life: The same item appears twice
- In JavaScript: The same property key
- In real life: The newer note wins
- In JavaScript: Later property wins
Where the analogy stops: Real lists can show both notes. Object spread keeps only the last value for a key.
Switch the merge order. The same two objects produce different winners because later properties overwrite earlier ones.
script
const userSettings = { theme: "dark" };const merged = { ...defaults, ...userSettings };console.log(merged.theme + " / " + merged.pageSize);This is why { ...user, name: "X" } and { name: "X", ...user } are not the same. In the first, name is definitely "X". In the second, user.name can overwrite it.
const safe = { ...null, ...undefined };const letters = { ..."JS" };const source = Object.create({ inherited: "skip" });Object.defineProperty(source, "hidden", { value: "skip", enumerable: false });Object.defineProperty(source, "label", { enumerable: true, get() { return "Ada"; },});const copy = { ...source };console.log(JSON.stringify(safe));console.log(JSON.stringify(letters));console.log(JSON.stringify(copy));console.log([...null]);The first three logs are {}, {"0":"J","1":"S"}, and {"label":"Ada"}. The final line throws a TypeError because array spread needs an iterable, and null is not iterable.
It copies own enumerable properties. It skips inherited and non-enumerable properties. If a copied property is a getter, the getter runs and the returned value becomes a plain value on the new object.
Rest in destructuring
STEP THROUGHDestructuring pulls values out of arrays or objects. Rest in a destructuring pattern collects whatever was not already pulled out. This is the opposite direction from spread.
Decide which dots are collecting leftovers. Then step through the object and array rest patterns.
script
const { password, ...safeUser } = user;const steps = ["draft", "review", "publish"];const [first, ...others] = steps;console.log(JSON.stringify(safeUser));console.log(first + " / " + others.join(", "));The object pattern { password, ...safeUser } is useful in real code because it omits one field and keeps the rest. The array pattern [first, ...others] is useful when the first item has a special meaning and the remaining items should stay grouped.
const [first, ...middle, last] = list; is a SyntaxError. Rest means “collect everything left,” so JavaScript does not allow another array or object binding after it in the same pattern.
Merging & copying
Spread creates a new outer container, but it is a shallow copy. That matches what you learned in the Objects & references lesson: object variables can share the same nested object.
const state = { user: { name: "Grace", role: "admin" }, tags: ["js", "data"]};const nextState = { ...state, user: { ...state.user, name: "Ada" }, tags: [...state.tags, "spread"]};console.log(state.user.name);console.log(nextState.user.name);console.log(state.tags === nextState.tags);The important part is copying every container on the path you change. The outer state is copied, the nested user object is copied before name changes, and tags is copied before appending. This is the style used by UI state tools such as React and Redux because new references make changes easy to detect.
| Tool | Depth | Good for | Watch out |
|---|---|---|---|
[...items] | Shallow | Copying or rebuilding arrays | Nested objects are shared |
{ ...object } | Shallow | Merging options and updating state | Later properties overwrite earlier ones |
Object.assign({}, a, b) | Shallow | Older object merge style | Also copies left to right; can mutate the first argument |
structuredClone(value) | Deep for supported values | Independent copies of structured data | Functions, DOM nodes, and some special values are not cloneable |
Where you’ll use this
Spread and rest appear anywhere you transform data without mutating the original. These patterns are small, but they show up constantly.
- Merge defaults with user settings:
{ ...defaults, ...userSettings }keeps missing defaults and lets user choices win. - Remove private fields:
const { password, ...safeUser } = user;keeps a clean object for display or logging. - Append immutably:
[...items, newItem]returns a new array. - Insert or remove with slices: combine
sliceand spread to rebuild the parts you want.
const inserted = [ ...items.slice(0, index), newItem, ...items.slice(index)];const removed = [ ...items.slice(0, index), ...items.slice(index + 1)];Order rules & pitfalls
Most mistakes come from expecting the dots to do more than they promise. Keep these rules close:
- Array and call spread keep the iterable’s order.
- Object spread copies left to right; later properties win.
- Rest in destructuring must be last because it collects all remaining values.
- Object spread copies only own enumerable properties.
- Spread copies are O(n) because JavaScript visits the values it copies.
Copying an array or object with spread is proportional to the number of entries copied. Spreading very large arrays into function calls can hit engine argument limits and throw a RangeError. For huge data, prefer a loop, reduce, or an API that accepts an array.
Common misconceptions
“Spread and rest are the same thing.”
They use the same token, but the direction is opposite. Spread expands; rest collects. The syntax around the dots tells you which one you have.
“Object spread makes a deep copy.”
It makes a new outer object only. Nested objects and arrays are still shared unless you copy those levels too or use a deep-copy tool.
“Object spread copies every property.”
It copies own enumerable properties. Inherited and non-enumerable properties are skipped, and getters are invoked during the copy.
“Spreading null always throws.”
Object spread skips null and undefined, but array spread with them throws because arrays and calls need iterables.
“Spread is free because it looks short.”
The syntax is short; the work is still a copy. For normal UI data that is fine. For huge lists, measure and choose a tool deliberately.
Practice: spread & rest
5 EXERCISESPredict first, then run the snippets in your console or editor. Each answer is checked against the real output.
What does the program print? Then run it to check.
const defaults = { theme: "light", density: "comfortable" };
const user = { density: "compact" };
const settings = { ...defaults, ...user };
console.log(settings.theme + " / " + settings.density);const defaults = { theme: "light", density: "comfortable" };
const user = { density: "compact" };
const settings = { ...defaults, ...user };
console.log(settings.theme + " / " + settings.density);settings.theme comes from defaults because the user object does not provide it. settings.density is compact because the user object is spread later.
Use object rest to keep safe data and leave the secret out.
const user = { name: "Ada", password: "secret", role: "admin" };
const { password, ...publicUser } = user;
console.log(Object.keys(publicUser).join(","));const user = { name: "Ada", password: "secret", role: "admin" };
const { password, ...publicUser } = user;
console.log(Object.keys(publicUser).join(","));password receives the secret value, and publicUser collects the leftover own enumerable properties: name and role.
Predict the output and notice that the old nested name stays unchanged.
const state = { user: { name: "Grace", score: 10 }, theme: "dark" };
const next = { ...state, user: { ...state.user, name: "Ada" } };
console.log(state.user.name + " -> " + next.user.name);const state = { user: { name: "Grace", score: 10 }, theme: "dark" };
const next = { ...state, user: { ...state.user, name: "Ada" } };
console.log(state.user.name + " -> " + next.user.name);The old state still says Grace, while the copied nested object says Ada. Copying both levels avoids mutating the original nested object.
Enter both printed lines in order.
const user = { name: "Ada" };
console.log(({ ...user, name: "Lin" }).name);
console.log(({ name: "Lin", ...user }).name);const user = { name: "Ada" };
console.log(({ ...user, name: "Lin" }).name);
console.log(({ name: "Lin", ...user }).name);The first object spreads user and then writes name: "Lin", so it prints Lin. The second writes Lin first and then spreads user, so it prints Ada.
Confirm that the old array and the new array print differently.
const items = ["draft", "review"];
const nextItems = [...items, "publish"];
console.log(items.join("/") + " -> " + nextItems.join("/"));const items = ["draft", "review"];
const nextItems = [...items, "publish"];
console.log(items.join("/") + " -> " + nextItems.join("/"));[...items, "publish"] expands the old elements into a new array and adds one more. The original array is unchanged.
Quiz: check your understanding
7 QUESTIONSEvery option explains itself. For code questions, read the dots from the surrounding syntax before you choose.
Question 1 of 7Which sentence is the best shortcut for remembering the two meanings of
...?Choose an answer to see the explanation.
Question 2 of 7What does the array spread snippet print?
Read the code, then predictconst a = ["x", "y"]; console.log([0, ...a, 3].join(""));Choose an answer to see the explanation.
Question 3 of 7What does the object merge snippet print?
Read the code, then predictconst defaults = { theme: "light", size: 20 }; const user = { theme: "dark" }; console.log(({ ...defaults, ...user }).theme);Choose an answer to see the explanation.
Question 4 of 7What does the object rest snippet print?
Read the code, then predictconst user = { name: "Ada", password: "secret", role: "admin" }; const { password, ...safeUser } = user; console.log(Object.keys(safeUser).join("/"));Choose an answer to see the explanation.
Question 5 of 7Which line is invalid JavaScript?
Read the code, then predictconst [first, ...middle, last] = [1, 2, 3];Choose an answer to see the explanation.
Question 6 of 7What does the shallow copy snippet print?
Read the code, then predictconst state = { user: { name: "Grace" } }; const copy = { ...state }; copy.user.name = "Ada"; console.log(state.user.name);Choose an answer to see the explanation.
Question 7 of 7Which statement about object spread is true?
Choose an answer to see the explanation.
Key takeaways
- Spread expands; rest collects.
- Array and call spread expand iterable values in order.
- Object spread copies own enumerable properties, and later properties win.
- Rest in destructuring collects leftovers and must be last.
- Spread copies are shallow and O(n); copy nested levels deliberately.
Remember the one-liner.
Spread expands values out; rest collects leftovers back in.
Up next: Map & Set, where you’ll choose collections made for unique values and flexible keys.