How to Use Rest Parameters and Spread Syntax in JavaScript
The three dots (...) are one of JavaScript's most versatile operators. Despite looking identical, they serve two opposite purposes depending on where they appear. When used in a function definition, ... gathers multiple values into a single array (rest parameters). When used in a function call, array literal, or object literal, ... expands an iterable into individual elements (spread syntax).
Mastering these two uses of ... will transform how you write functions, handle variable-length argument lists, copy arrays and objects, merge data structures, and destructure complex values. This guide covers everything you need to know, with clear examples showing both the problem each feature solves and how to use it correctly.
Rest Parameters: Gathering Arguments with ...args
Many functions need to accept a variable number of arguments. For example, a sum() function might need to work with 2, 5, or 100 numbers. Rest parameters let you capture all (or remaining) arguments as a real JavaScript array.
Basic Syntax
A rest parameter is declared by prefixing the last parameter name with ...:
function sum(...numbers) {
let total = 0;
for (const num of numbers) {
total += num;
}
return total;
}
console.log(sum(1, 2)); // 3
console.log(sum(1, 2, 3, 4, 5)); // 15
console.log(sum(10)); // 10
console.log(sum()); // 0
Inside the function, numbers is a real array. You can use all array methods on it: map, filter, reduce, forEach, and everything else.
function sum(...numbers) {
return numbers.reduce((acc, n) => acc + n, 0);
}
console.log(sum(1, 2, 3)); // 6
Gathering the "Remaining" Arguments
Rest parameters do not have to capture all arguments. You can have regular parameters first, and the rest parameter will gather only what is left:
function showWinner(first, second, ...others) {
console.log(`Gold: ${first}`);
console.log(`Silver: ${second}`);
console.log(`Other participants: ${others.join(", ")}`);
}
showWinner("Alice", "Bob", "Charlie", "Dave", "Eve");
// Gold: Alice
// Silver: Bob
// Other participants: Charlie, Dave, Eve
In this example, first gets "Alice", second gets "Bob", and others becomes the array ["Charlie", "Dave", "Eve"].
When there are no remaining arguments, the rest parameter is simply an empty array:
showWinner("Alice", "Bob");
// Gold: Alice
// Silver: Bob
// Other participants:
The Rest Parameter Must Be Last
The rest parameter must always be the last parameter in the function definition. Placing it anywhere else is a syntax error:
// WRONG: SyntaxError
function broken(...rest, last) {
// ...
}
// WRONG: SyntaxError
function alsoBroken(first, ...middle, last) {
// ...
}
// CORRECT: rest parameter is last
function correct(first, ...rest) {
// ...
}
There can be only one rest parameter in a function definition, and it must be the last parameter. This is a hard rule enforced by the JavaScript parser.
Practical Example: Building a Logger
function log(level, ...messages) {
const timestamp = new Date().toISOString();
const prefix = `[${timestamp}] [${level.toUpperCase()}]`;
console.log(prefix, ...messages);
}
log("info", "Server started on port", 3000);
// [2026-01-15T10:30:00.000Z] [INFO] Server started on port 3000
log("error", "Failed to connect to", "database", "after", 3, "retries");
// [2026-01-15T10:30:00.000Z] [ERROR] Failed to connect to database after 3 retries
Notice something interesting here: ...messages in the parameter list is rest (gathering), while ...messages inside console.log() is spread (expanding). Same variable, two different uses of ....
The arguments Object (Legacy) and Why Rest Is Better
Before rest parameters were introduced in ES2015, JavaScript functions had access to a special arguments object that contained all passed arguments. You will still encounter it in older codebases.
How arguments Works
function showArgs() {
console.log(arguments); // Arguments(3) ['a', 'b', 'c']
console.log(arguments[0]); // 'a'
console.log(arguments.length); // 3
}
showArgs("a", "b", "c");
The arguments object looks like an array and has numeric indexes and a length property, but it is not a real array. It is an "array-like" object.
The Problems with arguments
Problem 1: No array methods.
function sumOld() {
// WRONG: arguments.reduce is not a function
// return arguments.reduce((acc, n) => acc + n, 0);
// You had to convert it first
const args = Array.prototype.slice.call(arguments);
return args.reduce((acc, n) => acc + n, 0);
}
console.log(sumOld(1, 2, 3)); // 6
Problem 2: No way to separate named parameters from the rest.
function oldLog() {
// The first argument is the "level", the rest are messages
// But you have to manually slice
const level = arguments[0];
const messages = Array.prototype.slice.call(arguments, 1);
console.log(`[${level}]`, ...messages);
}
Compare with the clean rest parameter version:
function newLog(level, ...messages) {
console.log(`[${level}]`, ...messages);
}
Problem 3: Arrow functions do not have arguments.
const arrowFn = () => {
console.log(arguments); // ReferenceError (or inherits from outer function)
};
// With rest parameters, arrows work perfectly
const arrowFn2 = (...args) => {
console.log(args); // Real array, works great
};
arrowFn2(1, 2, 3); // [1, 2, 3]
Side-by-Side Comparison
| Feature | arguments | Rest Parameters ...args |
|---|---|---|
| Type | Array-like object | Real Array |
| Array methods | Not available directly | All methods available |
| Named params | No separation | Captures only remaining args |
| Arrow functions | Not available | Works perfectly |
| Readability | Unclear intent | Self-documenting |
| Modern usage | Legacy only | Recommended |
Always use rest parameters instead of arguments. The arguments object exists for backward compatibility, but there is no reason to use it in new code. Rest parameters are clearer, more powerful, and work in arrow functions.
Spread Syntax in Function Calls
Now we flip the perspective. While rest parameters collect values into an array, the spread syntax expands an iterable (arrays, strings, etc.) into individual values.
Passing Array Elements as Arguments
The most common use case is passing the elements of an array as separate arguments to a function:
const numbers = [3, 7, 1, 9, 4];
// Without spread: you would need apply()
console.log(Math.max.apply(null, numbers)); // 9
// With spread: clean and readable
console.log(Math.max(...numbers)); // 9
console.log(Math.min(...numbers)); // 1
Math.max expects individual arguments like Math.max(3, 7, 1, 9, 4), not an array. The spread syntax transforms the array into exactly that.
Combining Multiple Iterables
You can spread multiple arrays in the same function call, and mix them with regular arguments:
const frontEnd = [90, 85, 92];
const backEnd = [88, 95, 78];
console.log(Math.max(...frontEnd, ...backEnd)); // 95
console.log(Math.max(...frontEnd, 100, ...backEnd)); // 100
console.log(Math.max(50, ...frontEnd, 60, ...backEnd)); // 95
Spreading Strings
Strings are iterable, so spread works on them too. Each character becomes a separate argument:
function showChars(...chars) {
console.log(chars);
}
showChars(..."Hello");
// ['H', 'e', 'l', 'l', 'o']
Real-World Example: console.log with Context
function debug(label, ...data) {
console.log(`[DEBUG:${label}]`, ...data);
}
const info = ["user", { id: 42, name: "Alice" }, "logged in"];
debug("auth", ...info);
// [DEBUG:auth] user { id: 42, name: 'Alice' } logged in
Spread with Arrays: Copying, Merging, Converting
Spread syntax inside an array literal ([...]) is one of the most common patterns in modern JavaScript. It lets you copy, merge, and manipulate arrays in an immutable, readable way.
Copying an Array
const original = [1, 2, 3];
const copy = [...original];
copy.push(4);
console.log(original); // [1, 2, 3] (not affected)
console.log(copy); // [1, 2, 3, 4]
This creates a shallow copy. The array itself is new, but nested objects or arrays inside it are still shared by reference.
Spread creates a shallow copy, not a deep copy. Nested objects are shared between the original and the copy.
const users = [{ name: "Alice" }, { name: "Bob" }];
const usersCopy = [...users];
usersCopy[0].name = "Changed";
console.log(users[0].name); // "Changed" (the nested object was shared!)
For deep copies, use structuredClone().
Merging Arrays
const fruits = ["apple", "banana"];
const veggies = ["carrot", "pea"];
const grains = ["rice", "wheat"];
const allFood = [...fruits, ...veggies, ...grains];
console.log(allFood);
// ['apple', 'banana', 'carrot', 'pea', 'rice', 'wheat']
You can also insert elements between the spreads:
const withHeaders = ["--- Fruits ---", ...fruits, "--- Veggies ---", ...veggies];
console.log(withHeaders);
// ['--- Fruits ---', 'apple', 'banana', '--- Veggies ---', 'carrot', 'pea']
Adding Elements Immutably
Instead of mutating arrays with push, unshift, or splice, you can use spread to create new arrays with the modifications:
const items = [2, 3, 4];
// Add to the beginning (like unshift, but immutable)
const withFirst = [1, ...items];
console.log(withFirst); // [1, 2, 3, 4]
// Add to the end (like push, but immutable)
const withLast = [...items, 5];
console.log(withLast); // [2, 3, 4, 5]
// Insert in the middle
const withMiddle = [...items.slice(0, 2), 3.5, ...items.slice(2)];
console.log(withMiddle); // [2, 3, 3.5, 4]
// Remove by index (like splice, but immutable)
const indexToRemove = 1;
const withoutSecond = [...items.slice(0, indexToRemove), ...items.slice(indexToRemove + 1)];
console.log(withoutSecond); // [2, 4]
// Original is never modified
console.log(items); // [2, 3, 4]
This immutable pattern is especially important in frameworks like React, where you should never mutate state directly.
Converting Iterables to Arrays
Spread can convert any iterable into an array:
// String to array of characters
const chars = [..."Hello"];
console.log(chars); // ['H', 'e', 'l', 'l', 'o']
// Set to array (removes duplicates)
const unique = [...new Set([1, 2, 2, 3, 3, 3])];
console.log(unique); // [1, 2, 3]
// Map entries to array
const map = new Map([["a", 1], ["b", 2]]);
const entries = [...map];
console.log(entries); // [['a', 1], ['b', 2]]
// NodeList to array (in the browser)
// const divs = [...document.querySelectorAll("div")];
Spread vs. Array.from()
Both spread and Array.from() can convert iterables to arrays. The key difference:
- Spread (
[...x]) works only with iterables (objects withSymbol.iterator) Array.from(x)works with both iterables and array-likes (objects withlengthand indexed properties)
// Array-like object (has length and indexes, but no Symbol.iterator)
const arrayLike = { 0: "a", 1: "b", 2: "c", length: 3 };
// Spread FAILS
// const arr1 = [...arrayLike]; // TypeError: arrayLike is not iterable
// Array.from WORKS
const arr2 = Array.from(arrayLike);
console.log(arr2); // ['a', 'b', 'c']
Use Array.from() when you need to convert array-like objects (like arguments, NodeList, or custom objects with length). Use spread when you are working with known iterables and want a concise syntax.
Spread with Objects: Copying, Merging, Overriding
Spread syntax works with object literals too (since ES2018). It copies all own enumerable properties from one object into another.
Copying an Object
const user = { name: "Alice", age: 30, role: "admin" };
const userCopy = { ...user };
userCopy.name = "Bob";
console.log(user.name); // "Alice" (original not affected)
console.log(userCopy.name); // "Bob"
As with arrays, this is a shallow copy. Nested objects are still shared.
Merging Objects
const defaults = {
theme: "light",
language: "en",
notifications: true
};
const userPrefs = {
theme: "dark",
fontSize: 16
};
const settings = { ...defaults, ...userPrefs };
console.log(settings);
// {
// theme: 'dark', ← overridden by userPrefs
// language: 'en', ← from defaults
// notifications: true, ← from defaults
// fontSize: 16 ← from userPrefs
// }
When properties overlap, the last one wins. This is how you implement configuration defaults with user overrides.
Property Order Matters
Since the last property wins, the order of spreading determines which values take priority:
const base = { a: 1, b: 2, c: 3 };
const override = { b: 20, c: 30 };
// override wins for b and c
const result1 = { ...base, ...override };
console.log(result1); // { a: 1, b: 20, c: 30 }
// base wins for b and c (it comes last)
const result2 = { ...override, ...base };
console.log(result2); // { b: 2, c: 3, a: 1 }
Adding and Overriding Individual Properties
You can mix spread with regular property definitions:
const user = { name: "Alice", age: 30, role: "user" };
// Update some properties immutably
const updatedUser = {
...user,
role: "admin",
lastLogin: new Date()
};
console.log(updatedUser);
// { name: 'Alice', age: 30, role: 'admin', lastLogin: 2024-... }
console.log(user.role); // 'user' (original unchanged)
This pattern is fundamental in React state updates and Redux reducers:
// Redux-style state update
function reducer(state, action) {
switch (action.type) {
case "UPDATE_NAME":
return { ...state, name: action.payload };
case "INCREMENT_AGE":
return { ...state, age: state.age + 1 };
default:
return state;
}
}
Shallow Copy Warning with Nested Objects
This is one of the most common mistakes. Spread only copies one level deep:
const original = {
name: "Alice",
address: {
city: "New York",
zip: "10001"
}
};
const copy = { ...original };
copy.address.city = "Boston";
console.log(original.address.city); // "Boston" (MODIFIED!)
The address object is shared between original and copy. To avoid this, you need to spread nested objects explicitly:
const deeperCopy = {
...original,
address: { ...original.address }
};
deeperCopy.address.city = "Boston";
console.log(original.address.city); // "New York" (safe!)
For truly deep copies, use structuredClone():
const deepCopy = structuredClone(original);
deepCopy.address.city = "Boston";
console.log(original.address.city); // "New York"(safe!)
Spread Does Not Copy Non-Enumerable or Inherited Properties
const proto = { inherited: true };
const obj = Object.create(proto);
obj.own = "yes";
Object.defineProperty(obj, "hidden", {
value: "secret",
enumerable: false
});
const copy = { ...obj };
console.log(copy);
// { own: 'yes' }
// "inherited" is not copied (it's on the prototype)
// "hidden" is not copied (it's non-enumerable)
Rest/Spread with Destructuring
Rest and spread combine powerfully with destructuring assignment, allowing you to extract specific values and collect the remaining ones in a clean, expressive way.
Rest in Array Destructuring
const scores = [95, 88, 76, 91, 83];
const [highest, secondHighest, ...remaining] = scores;
console.log(highest); // 95
console.log(secondHighest); // 88
console.log(remaining); // [76, 91, 83]
You can skip elements and still collect the rest:
const [first, , third, ...others] = [1, 2, 3, 4, 5, 6];
console.log(first); // 1
console.log(third); // 3
console.log(others); // [4, 5, 6]
Rest in Object Destructuring
This is particularly useful for separating known properties from everything else:
const user = {
id: 1,
name: "Alice",
age: 30,
email: "alice@example.com",
role: "admin"
};
const { id, name, ...rest } = user;
console.log(id); // 1
console.log(name); // "Alice"
console.log(rest); // { age: 30, email: 'alice@example.com', role: 'admin' }
Practical Use: Removing Properties Immutably
You can use object rest destructuring to "remove" a property without mutating the original object:
const user = { name: "Alice", password: "secret123", age: 30 };
// Remove "password" and keep everything else
const { password, ...safeUser } = user;
console.log(safeUser); // { name: 'Alice', age: 30 }
console.log(user); // { name: 'Alice', password: 'secret123', age: 30 } (unchanged)
This pattern is incredibly common in real applications, for example when stripping sensitive data before sending a response to a client.
Practical Use: Flexible Function Parameters
A very powerful pattern is combining object destructuring with rest in function parameters:
function createUser({ name, email, ...options }) {
console.log("Name:", name);
console.log("Email:", email);
console.log("Other options:", options);
}
createUser({
name: "Alice",
email: "alice@example.com",
age: 30,
role: "admin",
theme: "dark"
});
// Name: Alice
// Email: alice@example.com
// Other options: { age: 30, role: 'admin', theme: 'dark' }
Practical Use: Forwarding Props (React Pattern)
In React, this pattern is used constantly to forward remaining props to child components:
function Button({ label, onClick, ...htmlProps }) {
// label and onClick are handled specifically
// everything else is forwarded to the <button> element
return (
<button onClick={onClick} {...htmlProps}>
{label}
</button>
);
}
// Usage:
// <Button label="Click me" onClick={handleClick} className="primary" disabled />
Nested Destructuring with Rest
You can combine nested destructuring and rest for complex data:
const response = {
status: 200,
data: {
user: { name: "Alice", age: 30 },
token: "abc123",
refreshToken: "xyz789"
},
headers: { "content-type": "application/json" }
};
const {
status,
data: { user, ...tokens },
...otherResponseFields
} = response;
console.log(status); // 200
console.log(user); // { name: 'Alice', age: 30 }
console.log(tokens); // { token: 'abc123', refreshToken: 'xyz789' }
console.log(otherResponseFields); // { headers: { 'content-type': 'application/json' } }
Combining Spread and Rest in One Operation
You can use rest to extract parts and spread to combine them:
const config = {
host: "localhost",
port: 3000,
debug: true,
verbose: false
};
// Extract debug settings, keep the rest as "serverConfig"
const { debug, verbose, ...serverConfig } = config;
// Create a new config with defaults for a different environment
const productionConfig = {
...serverConfig,
host: "production.example.com",
ssl: true
};
console.log(productionConfig);
// { host: 'production.example.com', port: 3000, ssl: true }
Common Mistakes and Gotchas
Mistake 1: Confusing Rest and Spread
// This is REST (gathering) (in a function DEFINITION)
function example(...args) {
// args is an array of all arguments
}
// This is SPREAD (expanding) (in a function CALL)
const nums = [1, 2, 3];
example(...nums); // Spreads array into separate arguments
The rule is simple: ... in a definition or pattern (function parameters, destructuring) is rest. ... in an expression (function calls, array/object literals) is spread.
Mistake 2: Spreading a Non-Iterable in an Array Context
const obj = { a: 1, b: 2 };
// WRONG: Objects are not iterable in array context
// const arr = [...obj]; // TypeError: obj is not iterable
// CORRECT: Use Object.entries, Object.keys, or Object.values
const entries = [...Object.entries(obj)]; // [['a', 1], ['b', 2]]
const keys = [...Object.keys(obj)]; // ['a', 'b']
const values = [...Object.values(obj)]; // [1, 2]
// CORRECT: Spread objects in object literals, not array literals
const copy = { ...obj }; // { a: 1, b: 2 }
Mistake 3: Expecting Deep Copy from Spread
const matrix = [[1, 2], [3, 4]];
const copy = [...matrix];
copy[0].push(99);
console.log(matrix[0]); // [1, 2, 99] (Original modified!)
// Fix: deep copy the inner arrays too
const deepCopy = matrix.map(row => [...row]);
deepCopy[0].push(99);
console.log(matrix[0]); // [1, 2] (Safe)
Mistake 4: Rest Parameter Not Last
// WRONG: SyntaxError
// function bad(first, ...middle, last) {}
// CORRECT: use destructuring if you need the last element
function workaround(...args) {
const first = args[0];
const last = args[args.length - 1];
const middle = args.slice(1, -1);
console.log({ first, middle, last });
}
workaround(1, 2, 3, 4, 5);
// { first: 1, middle: [2, 3, 4], last: 5 }
Quick Reference Summary
| Syntax | Context | Name | What It Does |
|---|---|---|---|
function f(...args) | Function parameter | Rest | Gathers arguments into an array |
f(...array) | Function call | Spread | Expands array into separate arguments |
[...arr1, ...arr2] | Array literal | Spread | Merges/copies arrays |
{...obj1, ...obj2} | Object literal | Spread | Merges/copies objects |
const [a, ...rest] = arr | Array destructuring | Rest | Gathers remaining elements |
const {a, ...rest} = obj | Object destructuring | Rest | Gathers remaining properties |
The core principle to remember: rest collects, spread expands. Rest appears in patterns (where you are receiving values). Spread appears in expressions (where you are providing values). Once this distinction is clear, the three dots become one of the most natural and powerful tools in your JavaScript code.