Skip to main content

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) {
// ...
}
danger

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

FeatureargumentsRest Parameters ...args
TypeArray-like objectReal Array
Array methodsNot available directlyAll methods available
Named paramsNo separationCaptures only remaining args
Arrow functionsNot availableWorks perfectly
ReadabilityUnclear intentSelf-documenting
Modern usageLegacy onlyRecommended
tip

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.

warning

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 with Symbol.iterator)
  • Array.from(x) works with both iterables and array-likes (objects with length and 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']
tip

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

SyntaxContextNameWhat It Does
function f(...args)Function parameterRestGathers arguments into an array
f(...array)Function callSpreadExpands array into separate arguments
[...arr1, ...arr2]Array literalSpreadMerges/copies arrays
{...obj1, ...obj2}Object literalSpreadMerges/copies objects
const [a, ...rest] = arrArray destructuringRestGathers remaining elements
const {a, ...rest} = objObject destructuringRestGathers 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.