Spread vs rest.
Same dots. Opposite purpose.
Both use .... Both are called "the spread operator" in casual speech. But they do opposite things. Spread expands a collection into individual elements. Rest collects individual elements into an array. The dots look identical; the context decides which one it is. This page covers both, when each applies, and the patterns you'll use every day.
Short answer: ... is spread when it appears in a function call, array literal, or object literal — it expands a collection into elements. ... is rest when it appears in a function parameter list or destructuring pattern — it collects elements into an array. Same syntax, opposite direction. Spread copies and merges. Rest collects remaining elements. Both are shallow — nested objects and arrays are still shared references.
✅ Spread — expands
Takes a collection, spreads it into elements.
const nums = [1, 2, 3];
const moreNums = [4, 5];
// Array literal — combine arrays
const combined = [...nums, ...moreNums];
console.log(combined);
// [1, 2, 3, 4, 5]
// Array literal — copy
const copy = [...nums];
copy.push(99);
console.log(nums); // [1, 2, 3] — unchanged
console.log(copy); // [1, 2, 3, 99]
// Object literal — merge
const user = { name: 'Alice', age: 30 };
const updated = { ...user, age: 31 };
console.log(updated);
// { name: 'Alice', age: 31 }
// Function call — spread as arguments
const max = Math.max(...nums);
console.log(max); // 3
const all = [...nums, 4, 5];
console.log(all);
// [1, 2, 3, 4, 5]
Spread in three contexts:
1. Array literal — [...arr]
→ expands arr into individual elements
→ can combine multiple arrays
→ creates a new array
2. Object literal — { ...obj }
→ expands obj into properties
→ later properties overwrite earlier ones
→ creates a new object
3. Function call — fn(...arr)
→ passes each element as a separate argument
→ equivalent to fn(arr[0], arr[1], arr[2])
The key: spread is about taking something
collected and expanding it.
Common patterns:
✅ Copy an array:
const copy = [...original];
✅ Combine arrays:
const all = [...a, ...b, ...c];
✅ Copy and modify an object:
const updated = { ...obj, key: newValue };
✅ Pass an array as arguments:
fn(...args);
✅ Clone with an additional element:
const more = [...arr, newItem];
All of these are shallow. Nested objects and
arrays are still the same references.
⚠️ The syntax is the same as rest — only
the context tells you which one it is.
✅ Rest — collects
Takes individual elements, collects them into an array.
// Function parameter — variable arguments
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6
console.log(sum(1, 2, 3, 4, 5)); // 15
// With other parameters — rest must be last
function greet(greeting, ...names) {
return names.map(n => ${greeting}, ${n});
}
console.log(greet('Hi', 'Alice', 'Bob'));
// ['Hi, Alice', 'Hi, Bob']
// Array destructuring — remaining elements
const [first, ...rest] = [1, 2, 3, 4];
console.log(first); // 1
console.log(rest); // [2, 3, 4]
// Object destructuring — remaining properties
const { name, ...others } = {
name: 'Alice',
age: 30,
city: 'Lahore'
};
console.log(name); // 'Alice'
console.log(others); // { age: 30, city: 'Lahore' }
Rest in three contexts:
1. Function parameters — fn(...args)
→ captures all remaining arguments
→ args is a real array
→ must be the last parameter
2. Array destructuring — [a, ...rest]
→ captures remaining elements
→ rest is an array
3. Object destructuring — { a, ...rest }
→ captures remaining properties
→ rest is a plain object
The key: rest is about taking things that
are separate and collecting them together.
Common patterns:
✅ Variable-argument functions:
function log(...args) { console.log(...args); }
✅ Extract and keep the rest:
const { password, ...safeUser } = user;
✅ Pass remaining props to a child:
const { children, ...rest } = props;
return {children};
✅ Skip known items in an array:
const [first, ...others] = items;
⚠️ Rest must be last:
function fn(...args, last) {} // SyntaxError
const [a, ...rest, b] = [1,2,3]; // SyntaxError
⚠️ Rest is a real array, not arguments:
function fn(...args) {
args.map(x => x * 2); // works
args.length; // works
}
Compare: arguments is array-like but not an array.
✅ The context tells you which
Same syntax, different role.
// SPREAD — appears inside a value
const copy = [...arr];
// [...arr] is a value being built.
// ...arr spreads arr into the new array.
// Spread.
fn(...arr);
// fn(...) is a call.
// ...arr spreads arr into arguments.
// Spread.
const obj = { ...source };
// { ...source } is a value being built.
// ...source spreads properties.
// Spread.
// REST — appears in a pattern
function fn(...args) {}
// ...args is a parameter declaration.
// It collects arguments into args.
// Rest.
const [...arr] = source;
// ...arr is a destructuring target.
// It collects remaining elements.
// Rest.
const { a, ...rest } = obj;
// ...rest is a destructuring target.
// It collects remaining properties.
// Rest.
// The rule:
// In a position that CREATES a value → spread
// In a position that CONSUMES a value → rest
// (This is not a spec rule — it's a mnemonic.)
How to tell:
Ask: is the ... on the left of an = or
inside a function's parameter list?
→ Rest. It's collecting.
Ask: is the ... on the right of an = ,
inside a function call, or inside
an array/object literal being built?
→ Spread. It's expanding.
Examples:
function fn(...args) → rest (parameter)
fn(...args) → spread (call)
const [...rest] = arr → rest (pattern)
const copy = [...arr] → spread (value)
const { a, ...rest } = o → rest (pattern)
const merged = { ...o } → spread (value)
Both use the same dots.
Both use the same characters.
Context is everything.
The mental model:
Spread: "unpack this collection"
Rest: "pack these into a collection"
Once you know which direction you want,
the context tells you which syntax applies.
In practice, you don't think about it —
you write what you mean, and the parser
does the right thing.
Don't confuse spread with Object.assign for mutation. Object.assign(target, source) mutates target. Spread creates a new object: { ...target, ...source }. If you're updating state in React, spread is correct — mutation breaks change detection. If you're writing a utility that needs to populate an existing object (like merging options into a config), Object.assign is fine and slightly faster. The rule: spread for new objects, Object.assign only when you specifically want to mutate an existing object in place. In modern code, spread wins almost every time.
⚠️ Common mistakes
The spread/rest mistakes that cost real time.
function fn(...args, x) {} — SyntaxError. Fix: rest must be the last parameter.
It's shallow — nested objects are still shared. Fix: use structuredClone for deep copies.
[...{ a: 1 }] — TypeError. Objects aren't iterable. Fix: [Object.values(obj)] or spread into an object.
[...'abc'] gives ['a', 'b', 'c']. Fix: use ['abc'] if you want one string.
Spread creates a new array. For million-element arrays, this is slow. Fix: pass the array directly if the API accepts one.
arguments in non-strict modeDeprecated pattern. Fix: use rest parameters.
const copy = [...arr]; copy.push(x); — works, but consider map or filter. Fix: use the operation that matches intent.
null or undefined[...null] — TypeError. Fix: [...(arr ?? [])].
❓ Frequently asked
What is the difference between spread and rest in JavaScript?
They use the same syntax (...) but do opposite things. Spread expands an iterable into individual elements: fn(...arr) passes each element as a separate argument. Rest collects individual elements into an array: function fn(...args) captures all arguments into an array. Spread is used in function calls, array literals, and object literals. Rest is used in function parameters and destructuring.
How do I use spread to copy an array?
const copy = [...original]. This creates a new array with all the elements of the original. It's a shallow copy — the elements are the same references. For nested objects or arrays, you need a deep copy (structuredClone, or a library).
How do I use spread to merge objects?
const merged = { ...obj1, ...obj2 }. This creates a new object with all the properties of both. If both objects have the same property, the later one wins. Spread doesn't deep-merge — nested objects are still references. For deep merge, use a library like lodash.merge.
What is the rest parameter?
The rest parameter captures all remaining arguments into an array: function fn(first, ...rest) { console.log(rest); }. It's how you write functions that accept a variable number of arguments. The rest parameter must be last in the parameter list. It replaces the old arguments object, and unlike arguments, it's a real array.
Can I use rest in destructuring?
Yes. In array destructuring: const [first, ...rest] = [1, 2, 3, 4]. In object destructuring: const { a, ...others } = { a: 1, b: 2, c: 3 }. This is the pattern for extracting specific properties and keeping the rest. Very common in React.
What is the difference between spread and Object.assign?
;They do the same thing for shallow merging objects, but with different syntax and one key difference. Object.assign(target, source) mutates the target object and returns it. Spread creates a new object. Use spread unless you specifically want to mutate the target.
🔗 Continue learning
Next in this series: