Free lesson · JavaScript Basic to Advance
Spread and Rest Operators
Three Dots, Two Behaviors
The ... syntax in modern JavaScript does two opposite things depending on where you write it. As spread, it expands an iterable into individual elements. As rest, it collects multiple elements into an array or object.
// Spread — expand
const numbers = [1, 2, 3];
const moreNumbers = [0, ...numbers, 4];
console.log(moreNumbers); // [0, 1, 2, 3, 4]
// Rest — collect
function sum(...args) {
return args.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3, 4, 5)); // 15
Same three dots. Different behaviors. The distinction depends on context: spread is used where multiple values can appear (function calls, array literals, object literals), and rest is used where you're gathering multiple values into one (function parameters, destructuring).
These operators are everywhere in modern JavaScript. Most array operations, function calls with variable arguments, immutable object updates, and React-style state updates use them. This Topic covers both, with the patterns where each shines.
Spread for Arrays
In an array literal, ...other inserts each element of other as a separate element.
const a = [1, 2, 3];
const b = [4, 5, 6];
// Concatenate
const combined = [...a, ...b];
console.log(combined); // [1, 2, 3, 4, 5, 6]
// Insert in the middle
const inserted = [...a, "middle", ...b];
console.log(inserted); // [1, 2, 3, "middle", 4, 5, 6]
// Copy an array
const copy = [...a];
console.log(copy);
console.log(copy === a); // false — different arrays
Notice the last point: [...a] is a shallow copy of the array. It's a new array with the same elements. Modifying the copy doesn't affect the original.
This is a critical pattern for working with immutable data. Instead of mutating arrays in place with push, pop, splice, you create new arrays with spread:
const original = [1, 2, 3];
// Old style — mutates
// original.push(4);
// Modern style — produces a new array
const updated = [...original, 4];
console.log(original); // [1, 2, 3] — unchanged
console.log(updated); // [1, 2, 3, 4]
Modern JavaScript style (especially in React, Redux, and most functional patterns) prefers this immutable approach. The original data doesn't change. Operations produce new data. This avoids the entire class of bugs caused by accidental mutation.
Interactive animation: Copy, concatenate and insert - spread pours each element into a new array literal, shown with the output.
Spread for Objects
In an object literal, ...other copies each own enumerable property of other into the new object:
const baseUser = {
name: "Olena",
role: "Head of Product",
active: true
};
// Copy an object
const userCopy = { ...baseUser };
console.log(userCopy);
console.log(userCopy === baseUser); // false
// Add or override properties
const updatedUser = { ...baseUser, age: 35, role: "CEO" };
console.log(updatedUser);
// { name: "Olena", role: "CEO", age: 35, active: true }
// 'role' was overridden because it appears after the spread
// Merge two objects
const settings = { theme: "dark" };
const merged = { ...baseUser, ...settings };
console.log(merged);
Two important rules:
Order matters. Properties later in the literal override earlier ones. { ...obj, key: "new" } makes key the new value. { key: "new", ...obj } lets obj override key if it has one.
Shallow copy. Same as with arrays — nested objects are not copied. The outer object is new, but inner references are shared:
const original = {
name: "Olena",
preferences: { theme: "dark", lang: "en" }
};
const copy = { ...original };
copy.name = "Nikhil"; // doesn't affect original
copy.preferences.theme = "light"; // DOES affect original!
console.log(original.preferences.theme); // "light" — shared inner reference
For deep copying, you need structuredClone(obj) (modern native), a library like Lodash, or manual recursive copying. Topic 7.7 covered this. The spread copy works for one level only.
Interactive animation: One level deep - the spread copy is a new object, but its nested object is still shared with the original.
Rest for Function Parameters
In a function's parameter list, ...args collects all remaining arguments into an array. This is the modern replacement for the older arguments object that traditional functions had.
function logAll(...args) {
console.log("Got", args.length, "arguments:", args);
}
logAll(1, 2, 3);
logAll("a", "b");
logAll();
// Combine with regular parameters
function greet(greeting, ...names) {
return names.map(n => `${greeting}, ${n}`).join("; ");
}
console.log(greet("Hi", "Olena", "Nikhil", "Emeka"));
The rest parameter must be the last parameter — you can't have anything after it. The first parameter takes the first argument, the rest collects all the others.
Compare with the old approach:
// Old — using arguments object
function oldSum() {
let total = 0;
for (let i = 0; i < arguments.length; i++) {
total += arguments[i];
}
return total;
}
// Modern — using rest
const newSum = (...nums) => nums.reduce((t, n) => t + n, 0);
console.log(oldSum(1, 2, 3));
console.log(newSum(1, 2, 3));
The modern version is clearer, works in arrow functions (which don't have arguments), and gives you a real array with full method access.
Rest in Destructuring
Rest also works in destructuring — to collect remaining values into an array (for arrays) or remaining properties into an object (for objects).
// Array destructuring with rest
const [first, second, ...others] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(second); // 2
console.log(others); // [3, 4, 5]
// Object destructuring with rest
const user = {
name: "Olena",
age: 35,
role: "Head of Product",
company: "Microsoft",
active: true
};
const { name, role, ...rest } = user;
console.log(name); // "Olena"
console.log(role); // "Head of Product"
console.log(rest); // { age: 35, company: "Microsoft", active: true }
This pattern is useful for "exclude these specific properties." const { password, ...safeUser } = user gives you a user object without the password — useful for logging or sending to clients without leaking sensitive fields.
Interactive animation: Pick some, gather the rest - password is pulled out and the remaining properties land in a new object.
Spread for Function Arguments
Spread also works at the call site of a function. It expands an array into individual arguments:
function add(a, b, c) {
return a + b + c;
}
const nums = [1, 2, 3];
// Without spread
console.log(add(nums[0], nums[1], nums[2]));
// With spread
console.log(add(...nums));
// Spread with Math functions
const values = [3, 1, 4, 1, 5, 9, 2, 6];
console.log(Math.max(...values)); // 9
console.log(Math.min(...values)); // 1
This is the modern replacement for func.apply(null, args). Spread at the call site is cleaner and works with any function. We saw a hint of this in Topic 8.1 when we covered apply.
graph LR
A[The ... operator] --> B[Spread<br/>expands into multiple]
A --> C[Rest<br/>collects multiple into one]
B --> D[Array/object literals]
B --> E[Function call arguments]
C --> F[Function parameters]
C --> G[Destructuring]
style A fill:#7c3aed,color:#fff
style B fill:#10b981,color:#fff
style C fill:#3b82f6,color:#fff
This diagram shows the two roles of the three-dot operator. As spread, it expands an iterable into multiple individual elements. You see it used in array literals, object literals, and function call arguments. As rest, it collects multiple elements into one — an array or object. You see it used in function parameters and in destructuring patterns. The same syntax serves opposite purposes depending on where it appears.
Common Patterns
A few idioms you'll see constantly:
Immutable array updates. Instead of mutating an array, build a new one with spread:
const todos = [
{ id: 1, text: "Buy milk" },
{ id: 2, text: "Write code" }
];
// Add an item
const withAdded = [...todos, { id: 3, text: "Exercise" }];
// Remove an item by id
const withoutOne = todos.filter(t => t.id !== 1);
// Update an item by id
const withUpdated = todos.map(t => t.id === 1 ? { ...t, text: "Buy oat milk" } : t);
console.log(withUpdated);
These three patterns — add, remove, update — handle most array state changes immutably.
Immutable object updates. Same idea for objects:
const user = { name: "Olena", age: 35, role: "Head of Product" };
// Update one property
const aged = { ...user, age: 36 };
// Update nested (be careful — this is still shallow)
const original = { user: { name: "Olena" }, settings: { theme: "dark" } };
const themed = { ...original, settings: { ...original.settings, theme: "light" } };
console.log(themed);
For nested updates, you have to spread at each level. This is one of the reasons libraries like Immer exist — to make deep immutable updates cleaner. But for shallow cases, the spread pattern is the standard.
Interactive animation: The accidental quadratic - spreading into a new array inside a loop, compared side by side with push.
Common Pitfalls
Warning: The spread/rest traps.
Forgetting that spread is shallow. Nested objects and arrays are not copied — only the outer level. Mutating nested values affects the original. Use
structuredCloneor a deep-clone utility for true copies.Rest must be last.
function f(...args, last)is a syntax error. Rest must be the final parameter. The same applies in destructuring: rest must be the last element.Spreading non-iterables.
[...obj]whereobjisn't iterable throws. Arrays, strings, Sets, Maps, and Generators are iterable. Plain objects are not (useObject.entriesfirst). The error is "X is not iterable."Order matters in object spread.
{ ...obj, key: "new" }and{ key: "new", ...obj }produce different results. Spread later if you want to override; spread earlier if you want defaults.
Interactive animation: Last write wins - the same spreads in swapped order give different results, so order is the override policy.
Why This Matters in Interviews
Interview: Spread and rest fluency is expected in modern JavaScript interviews. The patterns — immutable updates, function arguments, destructuring rest — appear constantly in coding problems.
Senior questions sometimes probe the shallow-copy issue: "What's the difference between
{ ...obj }andstructuredClone(obj)?" The answer involves depth of copy.Topic 10.7 — Interview Lens: Modern JavaScript Patterns — covers the deeper treatment.
Try It
Exercise: Solve each problem using only spread/rest patterns. No mutation. No loops.
- Combine two arrays into one without modifying either
- Add an element to the front of an array (immutably)
- Remove the second element from an array (immutably)
- Merge two objects, with the second object's properties winning on collisions
- Build a function
sum(...nums)that adds any number of arguments- Destructure an object into
name,age, andrest(everything else)Each one is a single line. By the time you can solve them all without thinking, spread/rest is internalized.