JavaScript Spread and Rest Operators
JavaScript's `...` operator is simultaneously one of the language's most elegant features and a source of confusion for developers. The same three-dot syntax performs two fundamentally different...
Key Insights
- The
...syntax serves two opposite purposes: spread expands iterables into individual elements, while rest collects multiple elements into a single array—context determines which operation occurs - Spread and rest operators only perform shallow copies, meaning nested objects and arrays are copied by reference, not by value—a common source of bugs in state management
- Rest parameters must always be the last parameter in a function signature and can only be used once, while spread can be used multiple times in the same expression
The Three Dots That Do Two Things
JavaScript’s ... operator is simultaneously one of the language’s most elegant features and a source of confusion for developers. The same three-dot syntax performs two fundamentally different operations depending on where you use it. When you’re expanding or unpacking values, it’s the spread operator. When you’re collecting or gathering values, it’s the rest operator.
Here’s the key distinction:
// Spread: expanding an array into individual elements
const numbers = [1, 2, 3];
console.log(...numbers); // 1 2 3
// Rest: collecting multiple arguments into an array
function sum(...numbers) {
return numbers.reduce((total, n) => total + n, 0);
}
console.log(sum(1, 2, 3)); // 6
The context is everything. Spread takes something that’s grouped together and breaks it apart. Rest takes things that are separate and groups them together. Once you internalize this inverse relationship, the syntax becomes intuitive.
The Spread Operator: Expanding Iterables
Spread operators work with any iterable—arrays, strings, sets, maps, and objects (though objects aren’t technically iterable, spread syntax still works with them). The most common use case is array manipulation.
Array Operations
Spreading arrays gives you a clean syntax for concatenation and cloning:
const fruits = ['apple', 'banana'];
const vegetables = ['carrot', 'spinach'];
// Concatenation
const food = [...fruits, ...vegetables];
// ['apple', 'banana', 'carrot', 'spinach']
// Cloning (shallow copy)
const fruitsCopy = [...fruits];
// Adding elements while copying
const moreFruits = [...fruits, 'orange', 'grape'];
This is cleaner than using concat() or slice() for copying. You can also spread arrays into function arguments:
const numbers = [5, 2, 8, 1, 9];
console.log(Math.max(...numbers)); // 9
// Equivalent to: Math.max(5, 2, 8, 1, 9)
Object Spreading
Object spreading is invaluable for creating modified copies without mutation:
const defaults = {
theme: 'dark',
fontSize: 14,
notifications: true
};
const userPrefs = {
fontSize: 16,
language: 'es'
};
// Merge objects (later properties override earlier ones)
const config = { ...defaults, ...userPrefs };
// { theme: 'dark', fontSize: 16, notifications: true, language: 'es' }
Property order matters. Properties from objects spread later override earlier ones:
const override = { ...userPrefs, ...defaults };
// { fontSize: 14, language: 'es', theme: 'dark', notifications: true }
// fontSize is now 14, not 16
String Spreading
Spreading strings converts them into character arrays:
const word = 'hello';
const chars = [...word];
// ['h', 'e', 'l', 'l', 'o']
// Useful for removing duplicates
const unique = [...new Set('mississippi')];
// ['m', 'i', 's', 'p']
The Rest Operator: Collecting Values
While spread expands, rest collects. It’s primarily used in two contexts: function parameters and destructuring.
Rest Parameters in Functions
Rest parameters let you write variadic functions that accept any number of arguments:
function createUser(name, email, ...roles) {
return {
name,
email,
roles // roles is an array of all remaining arguments
};
}
const user = createUser('Alice', 'alice@example.com', 'admin', 'editor', 'reviewer');
// { name: 'Alice', email: 'alice@example.com', roles: ['admin', 'editor', 'reviewer'] }
Rest parameters must be the last parameter—this won’t work:
// ❌ SyntaxError
function invalid(...rest, last) { }
// ✅ Correct
function valid(first, ...rest) { }
Array Destructuring with Rest
Rest syntax collects remaining array elements during destructuring:
const [first, second, ...remaining] = [1, 2, 3, 4, 5];
console.log(first); // 1
console.log(second); // 2
console.log(remaining); // [3, 4, 5]
// Skip elements with empty slots
const [head, , ...tail] = [1, 2, 3, 4];
console.log(head); // 1
console.log(tail); // [3, 4]
Object Destructuring with Rest
Extract specific properties while collecting the rest:
const user = {
id: 1,
name: 'Bob',
email: 'bob@example.com',
age: 30,
city: 'Seattle'
};
const { id, email, ...profile } = user;
console.log(id); // 1
console.log(email); // 'bob@example.com'
console.log(profile); // { name: 'Bob', age: 30, city: 'Seattle' }
This is particularly useful for removing properties:
const { password, ...safeUser } = userFromDatabase;
// Send safeUser to the client without the password
Practical Use Cases and Patterns
Immutable Array Updates
When working with state management libraries like Redux or React’s useState, you need immutable updates:
// Add item to array
const todos = ['task1', 'task2'];
const withNewTodo = [...todos, 'task3'];
// Remove item by index
const index = 1;
const withoutTodo = [...todos.slice(0, index), ...todos.slice(index + 1)];
// Update item at index
const updated = [...todos.slice(0, index), 'updated task', ...todos.slice(index + 1)];
// Prepend item
const withPrepended = ['task0', ...todos];
Configuration Merging with Defaults
A common pattern in libraries and APIs:
function fetchData(url, options = {}) {
const defaults = {
method: 'GET',
headers: { 'Content-Type': 'application/json' },
timeout: 5000
};
const config = { ...defaults, ...options };
// User options override defaults
return fetch(url, config);
}
fetchData('/api/users', { method: 'POST', timeout: 10000 });
Extracting and Forwarding Props
Common in React component composition:
function Button({ variant, size, ...htmlProps }) {
const className = `btn btn-${variant} btn-${size}`;
// Forward all other props to the button element
return <button className={className} {...htmlProps} />;
}
// Usage: onClick, disabled, etc. are forwarded
<Button variant="primary" size="lg" onClick={handleClick} disabled={isLoading} />
Function Composition
Collect arguments and pass them along:
function logger(fn) {
return function(...args) {
console.log(`Calling with args:`, args);
const result = fn(...args);
console.log(`Result:`, result);
return result;
};
}
const add = (a, b) => a + b;
const loggedAdd = logger(add);
loggedAdd(2, 3); // Logs arguments and result
Common Pitfalls and Best Practices
Shallow Copy Gotcha
The biggest trap: spread only creates shallow copies. Nested objects are copied by reference:
const original = {
name: 'Alice',
settings: {
theme: 'dark'
}
};
const copy = { ...original };
copy.settings.theme = 'light';
console.log(original.settings.theme); // 'light' - Oops!
For deep copying, you need a different approach:
// Option 1: Manual deep copy for specific properties
const copy = {
...original,
settings: { ...original.settings }
};
// Option 2: structuredClone (modern browsers)
const deepCopy = structuredClone(original);
// Option 3: Library (lodash)
const deepCopy = _.cloneDeep(original);
Rest Parameter Positioning
Rest must be last, and you can only have one:
// ❌ Wrong
function wrong(...rest, last) { }
function alsoWrong(...rest1, ...rest2) { }
// ✅ Correct
function correct(first, second, ...rest) { }
Performance Considerations
Spreading large arrays or objects has a cost. For performance-critical code with large datasets, consider alternatives:
// Slow for large arrays
const combined = [...array1, ...array2];
// Faster for large arrays
const combined = array1.concat(array2);
// Even better if you don't need a new array
array1.push(...array2); // Mutates array1
Object Property Ordering
Object spread maintains property order, but be aware of numeric keys:
const obj = { ...{ 2: 'two', 1: 'one', a: 'a' } };
// Numeric keys are sorted: { 1: 'one', 2: 'two', a: 'a' }
Conclusion
Spread and rest operators have become essential tools in modern JavaScript. They enable cleaner, more expressive code for common operations like copying, merging, and function argument handling. Remember that spread expands and rest collects, both perform shallow operations, and rest must always come last. Master these operators, and you’ll write more concise and maintainable JavaScript.