Spread and Rest Operators in JavaScript
JavaScript's spread and rest operators use the same syntax: three dots (...). At first glance, this can be confusing—how can one syntax do two different things? The key is context. When you use ... on the right side of an assignment or in a function call, it's the spread operator. When you use it on the left side of an assignment or in function parameters, it's the rest operator. This guide will clarify both and show you practical ways to use them.
What the Spread Operator Does
The spread operator expands an iterable (like an array or object) into individual elements. Think of it as "unpacking" a collection—wherever you write ...myArray, JavaScript replaces it with each element from that array.
const numbers = [1, 2, 3];
console.log(...numbers); // Output: 1 2 3
This is most useful when you need to pass the elements of an array as separate arguments to a function:
const numbers = [1, 2, 3];
// Without spread - passes the whole array as one argument
console.log(Math.max(numbers)); // Output: NaN
// With spread - passes each element as separate arguments
console.log(Math.max(...numbers)); // Output: 3
The spread operator works with any iterable, including strings. You can spread a string to get individual characters:
const greeting = "hello";
console.log(...greeting); // Output: h e l l o
It also works with Sets, which are iterable collections:
const uniqueNumbers = new Set([1, 2, 2, 3, 3]);
console.log(...uniqueNumbers); // Output: 1 2 3
What the Rest Operator Does
The rest operator collects multiple elements into a single array or object. It's the opposite of spread—it packs elements together instead of unpacking them.
The rest operator appears in two main contexts: function parameters and destructuring assignments.
In Function Parameters
When used in function parameters, rest collects all passed arguments into an array:
function sum(...numbers) {
return numbers.reduce((total, num) => total + num, 0);
}
console.log(sum(1, 2, 3)); // Output: 6
console.log(sum(1, 2, 3, 4, 5)); // Output: 15
console.log(sum()); // Output: 0
The function doesn't need to know how many arguments will be passed—it just collects whatever comes in.
In Destructuring
Rest also works when destructuring arrays or objects:
// Rest with arrays
const [first, second, ...rest] = [1, 2, 3, 4, 5];
console.log(first); // Output: 1
console.log(second); // Output: 2
console.log(rest); // Output: [3, 4, 5]
// Rest with objects
const { name, age, ...others } = { name: "Alice", age: 30, city: "NYC", country: "USA" };
console.log(name); // Output: "Alice"
console.log(age); // Output: 30
console.log(others); // Output: { city: "NYC", country: "USA" }
Key Differences Between Spread and Rest
The difference comes down to direction and purpose:
| Aspect | Spread Operator | Rest Operator |
|---|---|---|
| Purpose | Expand/unwrap | Collect/pack |
| Direction | Right side of assignment | Left side of assignment |
| Location | Arrays in function calls, object literals | Function parameters, destructuring |
| Result | Individual elements | Array of elements |
Think of spread as "spreading out" (expanding) and rest as "gathering the rest" (collecting).
// SPREAD - unpacking
const a = [1, 2];
const b = [3, 4];
const combined = [...a, ...b]; // [1, 2, 3, 4]
// REST - packing
function printAll(...args) {
console.log(args); // Array of all arguments
}
Using Spread with Arrays
The spread operator makes working with arrays much cleaner. Here are the most common patterns.
Combining Arrays
Instead of using concat() or push(), you can merge arrays with spread:
const fruits = ["apple", "banana"];
const vegetables = ["carrot", "lettuce"];
// Combine two arrays
const produce = [...fruits, ...vegetables];
console.log(produce); // ["apple", "banana", "carrot", "lettuce"]
// Add elements before and after
const moreProduce = ["tomato", ...fruits, "cucumber", ...vegetables];
console.log(moreProduce); // ["tomato", "apple", "banana", "cucumber", "carrot", "lettuce"]
Copying Arrays
Spread creates a shallow copy of an array. This is useful when you want to create a modified version without changing the original:
const original = [1, 2, 3];
const copy = [...original];
console.log(copy); // [1, 2, 3]
copy.push(4);
console.log(original); // [1, 2, 3] - unchanged
console.log(copy); // [1, 2, 3, 4]
Important: spread only creates a shallow copy. If your array contains objects, the objects themselves are still shared:
const original = [{ name: "Alice" }, { name: "Bob" }];
const copy = [...original];
copy[0].name = "Charlie";
console.log(original[0].name); // "Charlie" - nested object is shared!
Converting to Array
Strings and other iterables can be converted to arrays using spread:
const greeting = "hello";
const letters = [...greeting];
console.log(letters); // ["h", "e", "l", "l", "o"]
// Works with any iterable
const set = new Set([1, 2, 3]);
const setToArray = [...set];
console.log(setToArray); // [1, 2, 3]
Removing Duplicates
Combined with Set, spread provides an easy way to remove duplicates:
const withDuplicates = [1, 2, 2, 3, 3, 3, 4];
const unique = [...new Set(withDuplicates)];
console.log(unique); // [1, 2, 3, 4]
Using Spread with Objects
The spread operator works with objects too, which is particularly useful for creating modified copies.
Copying Objects
Just like arrays, objects can be shallow copied:
const person = { name: "Alice", age: 30 };
// Create a copy
const personCopy = { ...person };
console.log(personCopy); // { name: "Alice", age: 30 }
// Modify the copy without affecting original
personCopy.city = "NYC";
console.log(person); // { name: "Alice", age: 30 }
console.log(personCopy); // { name: "Alice", age: 30, city: "NYC" }
Merging Objects
Object spread lets you merge multiple objects:
const defaults = { theme: "dark", language: "en" };
const userPrefs = { theme: "light" };
// User preferences override defaults
const settings = { ...defaults, ...userPrefs };
console.log(settings); // { theme: "light", language: "en" }
When there's a conflict, the later object wins. In this example, theme: "light" from userPrefs overrides theme: "dark" from defaults.
Adding or Overriding Properties
Spread makes it easy to create modified versions of objects:
const user = { name: "Alice", age: 30, city: "NYC" };
// Add new property
const userWithEmail = { ...user, email: "alice@example.com" };
// Override existing property
const olderUser = { ...user, age: 31 };
// Reorder properties (note: name becomes last)
const reordered = { city: user.city, name: user.name, age: user.age };
Practical Example: Form Updates
A common pattern in React or similar frameworks is updating part of an object:
const formState = {
name: "Alice",
email: "alice@example.com",
password: "secret123"
};
// User changes their name
const updatedForm = {
...formState,
name: "Alice Smith"
};
Practical Use Cases
Let's walk through real-world scenarios where spread and rest prove invaluable.
Flexible Function Arguments
Rest allows functions to accept any number of arguments:
function logMessages(...messages) {
messages.forEach((msg, i) => {
console.log(`${i + 1}. ${msg}`);
});
}
logMessages("Hello", "World", "!");
// Output:
// 1. Hello
// 2. World
// 3. !
Collecting Remaining Properties
When destructuring, rest captures everything else:
function parseUser({ name, age, ...details }) {
console.log(`${name} is ${age} years old`);
console.log("Additional info:", details);
}
parseUser({ name: "Alice", age: 30, city: "NYC", country: "USA" });
// Output:
// Alice is 30 years old
// Additional info: { city: "NYC", country: "USA" }
Passing Array as Arguments
Convert an array of values into separate function arguments:
const numbers = [3, 1, 4, 1, 5, 9, 2, 6];
// Math.min expects separate arguments, not an array
console.log(Math.min(...numbers)); // Output: 1
console.log(Math.max(...numbers)); // Output: 9
// Compare to without spread
console.log(Math.min(numbers)); // Output: NaN
Object Updates Without Mutation
Create new objects with specific changes:
const state = {
users: [],
filter: "all",
page: 1
};
// Update without changing original
const newState = {
...state,
page: 2,
filter: "active"
};
Function Composition
Build higher-order functions that accept any number of transformations:
function pipe(...functions) {
return function(value) {
return functions.reduce((result, fn) => fn(result), value);
};
}
const addOne = x => x + 1;
const double = x => x * 2;
const square = x => x * x;
const transform = pipe(addOne, double, square);
console.log(transform(3)); // (3 + 1) * 2 = 8, 8 * 8 = 64
Replacing apply Calls
Before arrow functions, you'd use Function.prototype.apply to spread arrays as arguments:
// Old way with apply
const max = Math.max.apply(null, [1, 2, 3, 4, 5]);
// Modern way with spread
const maxModern = Math.max(...[1, 2, 3, 4, 5]);
The spread syntax is cleaner and more readable.
Combining Default Config with User Config
A common pattern for configuration:
const defaultConfig = {
timeout: 5000,
retries: 3,
apiKey: "default-key"
};
function makeRequest(userConfig = {}) {
const config = { ...defaultConfig, ...userConfig };
console.log(config);
}
makeRequest(); // Uses all defaults
makeRequest({ timeout: 2000 }); // Only overrides timeout
Common Mistakes to Avoid
Understanding the difference between spread and rest helps avoid confusion.
Trying to spread a non-iterable:
// This fails - objects aren't iterable by default
const obj = { a: 1 };
// console.log(...obj); // Error!
// But you can spread an object's own properties into another object
const newObj = { ...obj };
Confusing location:
// Spread (right side) - unpacks
const arr = [1, 2];
const expanded = [...arr]; // [1, 2]
// Rest (left side) - packs
function fn(...args) { } // args is an array
// In destructuring:
const [first, ...remaining] = [1, 2, 3]; // remaining is [2, 3]
Forgetting order matters with objects:
const obj1 = { a: 1, b: 2 };
const obj2 = { b: 3, c: 4 };
// Later values override earlier ones
const merged = { ...obj1, ...obj2 };
console.log(merged); // { a: 1, b: 3, c: 4 }
Suggestions for Using Spread and Rest
Prefer spread for creating modified copies. Instead of mutating arrays or objects directly, use spread to create new versions:
// Instead of
arr.push(newItem);
// Use
const newArr = [...arr, newItem];
Use rest for variadic functions. Functions that accept any number of arguments should use rest parameters:
// Instead of
function sum() { return Array.from(arguments).reduce(...); }
// Use
function sum(...numbers) { return numbers.reduce(...); }
Use destructuring with rest for cleaner parameter handling. Instead of accessing properties individually:
// Instead of
function handleUser(user) {
const name = user.name;
const age = user.age;
const email = user.email;
}
// Use
function handleUser({ name, age, email }) {
// Direct access to name, age, email
}
Keep the order logical. When merging objects, think about which values should take precedence:
const merged = { ...defaults, ...overrides };
Use spread for immutability patterns. In React and similar frameworks, spreading helps maintain immutability:
setState(prevState => ({
...prevState,
newField: newValue
}));
Remember shallow vs deep copy. Spread creates shallow copies—both original and copy share references to nested objects. For deep copies, you'll need a library or JSON.parse(JSON.stringify()) (though that has limitations with functions and special values).
Quick Reference
| Operation | Code | Result |
|---|---|---|
| Copy array | [...arr] |
New array with same elements |
| Copy object | { ...obj } |
New object with same properties |
| Merge arrays | [...arr1, ...arr2] |
Combined array |
| Merge objects | { ...obj1, ...obj2 } |
Combined object |
| Function rest | function(...args) |
args is array of all arguments |
| Destructure rest | const [a, ...rest] = arr |
rest is remaining elements |
Conclusion
The spread and rest operators, despite using identical syntax (...), serve opposite purposes. Spread expands iterables into individual elements—it's the "unpack" operation. Rest collects multiple elements into a single array—it's the "pack" operation.
Understanding the context determines which you're using: on the right side of an assignment or in function calls, it's spread; on the left side of an assignment or in function parameters, it's rest. Once this distinction clicks, you'll find yourself using these operators constantly for cleaner, more expressive code.
The practical applications are everywhere: combining arrays and objects, creating copies without mutation, accepting flexible arguments in functions, and handling unknown numbers of parameters gracefully. Both operators are fundamental tools that will make your JavaScript code more concise and maintainable.
Top comments (0)