Rest Parameters in JavaScript
Rest parameters in JavaScript turn a flexible argument list into a real array. That is the important part. They are not just modern decoration for arguments, and they are not the same thing as spread syntax. Rest gathers values at the function boundary so the function can use normal array logic inside.
Use rest parameters when a function should accept any number of values with one clear rule for how those values are handled.
The basic syntax
A rest parameter starts with ... and appears in the function parameter list:
function totalScores(...scores) {
let total = 0;
for (const score of scores) {
total += score;
}
return total;
}
console.log(totalScores(10, 15, 20)); // 45
console.log(totalScores(4, 6)); // 10
console.log(totalScores()); // 0
Inside the function, scores is an array. You can loop over it, read its length, call array methods on it, and pass it to other functions.
function highestScore(...scores) {
if (scores.length === 0) {
return 0;
}
return Math.max(...scores);
}
console.log(highestScore(12, 18, 7)); // 18
console.log(highestScore()); // 0
That example also uses spread syntax in the Math.max call. The two ... forms point in opposite directions. Rest gathers. Spread expands.
For the expanding side, read JavaScript Spread Operator.
Rest replaces the old arguments object
Before rest parameters, flexible functions often used arguments:
function oldTotalScores() {
let total = 0;
for (const score of arguments) {
total += score;
}
return total;
}
That works, but arguments is an odd old object. It is array-like, not a real array:
function brokenDoubleScores() {
return arguments.map((score) => score * 2);
}
brokenDoubleScores(10, 20);
// TypeError: arguments.map is not a function
With a rest parameter, the array behavior is ordinary:
function doubleScores(...scores) {
return scores.map((score) => score * 2);
}
console.log(doubleScores(10, 20)); // [20, 40]
Rest parameters are also explicit. The function signature tells the reader that the function accepts a flexible number of values. arguments hides that inside the body, like a little surprise nobody ordered.
Arrow functions do not have their own arguments
Rest parameters matter even more with arrow functions because arrow functions do not create their own arguments object:
const totalScores = (...scores) => {
return scores.reduce((total, score) => total + score, 0);
};
console.log(totalScores(10, 15, 20)); // 45
Trying to use arguments inside an arrow function reads from the surrounding scope, not from the arrow call. That is rarely what you want.
For more on that behavior, see JavaScript Arrow Functions.
The rest parameter must be last
A rest parameter collects every remaining argument, so JavaScript has to know where that collection starts. It must be the final parameter:
function logMessages(prefix, ...messages) {
for (const message of messages) {
console.log(`${prefix}: ${message}`);
}
}
logMessages("info", "loaded", "ready");
// "info: loaded"
// "info: ready"
This is invalid:
function broken(...messages, prefix) {
return messages.length;
}
// SyntaxError: Rest parameter must be last formal parameter
Once ...messages starts collecting, there is no sensible positional argument left for prefix.
Fixed parameters and rest parameters work together
Rest parameters are not only for functions where every argument is the same kind of value. They are often most useful after one or two fixed parameters:
function buildUrl(baseUrl, ...pathParts) {
const path = pathParts
.map((part) => String(part).replace(/^\/+|\/+$/g, ""))
.join("/");
return `${baseUrl.replace(/\/+$/g, "")}/${path}`;
}
console.log(buildUrl("https://example.com/", "/users/", 42, "settings"));
// "https://example.com/users/42/settings"
baseUrl is required. Everything after it is collected into pathParts.
This pattern keeps the function signature honest:
function sendEvent(name, ...details) {
return {
name,
details,
};
}
console.log(sendEvent("course_started", "foundations", "lesson-09"));
// { name: "course_started", details: ["foundations", "lesson-09"] }
The fixed parameter names the required concept. The rest parameter names the variable tail.
Rest is not spread
Rest and spread both use ..., which is rude but survivable. The position tells you what is happening.
Rest appears in a function parameter list and gathers arguments:
function collect(...items) {
return items;
}
console.log(collect("a", "b", "c")); // ["a", "b", "c"]
Spread appears in a call, array literal, or object literal and expands a value:
const letters = ["a", "b", "c"];
console.log(collect(...letters)); // ["a", "b", "c"]
console.log(["start", ...letters, "end"]); // ["start", "a", "b", "c", "end"]
A useful way to remember it:
- Rest collects separate arguments into one array.
- Spread expands one iterable into separate values.
Same punctuation. Opposite movement.
Empty input needs a contract
Rest parameters make flexible input easy, but the function still needs to decide what empty input means.
This average function has a bug:
function averageScore(...scores) {
const total = scores.reduce((sum, score) => sum + score, 0);
return total / scores.length;
}
console.log(averageScore()); // NaN
The rest array is empty, so scores.length is 0. Dividing 0 / 0 gives NaN.
Give the empty case an explicit result:
function averageScore(...scores) {
if (scores.length === 0) {
return 0;
}
const total = scores.reduce((sum, score) => sum + score, 0);
return total / scores.length;
}
console.log(averageScore()); // 0
console.log(averageScore(10, 20, 30)); // 20
Rest parameters answer "how many arguments can this function receive?" They do not answer "what should zero arguments mean?" That part is still your job.
Forwarding and partial application
Rest parameters are useful when you want to collect some arguments now and pass them later.
function partialLeft(fn, ...presetArgs) {
return function (...laterArgs) {
return fn(...presetArgs, ...laterArgs);
};
}
const join = (a, b, c) => `${a}-${b}-${c}`;
const addTicketPrefix = partialLeft(join, "ticket");
console.log(addTicketPrefix("bug", 42)); // "ticket-bug-42"
The outer function gathers the arguments to remember. The returned function gathers the arguments supplied later. The call to fn uses spread to pass both arrays as normal positional arguments.
This pattern shows up in small utility functions, event wrappers, function pipelines, and higher-order functions. For the callback side of that world, read Higher-Order Functions in JavaScript.
When not to use a rest parameter
Do not use rest when the function actually has a fixed shape:
function createUser(...values) {
const [name, email, role] = values;
return { name, email, role };
}
That function accepts three meaningful inputs. Hiding them inside values makes the signature less useful:
function createUser(name, email, role = "member") {
return { name, email, role };
}
Use named parameters when each position has a specific meaning. Use rest when the remaining arguments are a collection.
For that default role fallback, see Default Parameters in JavaScript.
Key Takeaways
- Rest parameters gather remaining arguments into a real array.
- They are clearer and more array-friendly than the old
argumentsobject. - A rest parameter must be the last parameter because it collects the rest of the argument list.
- Rest gathers values into a function; spread expands values out into calls, arrays, or objects.
- Flexible input still needs an explicit empty-case contract.