JAVASCRIPT / FUNCTIONS
Rest parameters and variadic functions
Write and call variadic JavaScript functions with a rest parameter, and predict the array it builds, where it must sit, and how it affects fn.length.
What you will learn
- Declare a variadic function by putting ...name last in the parameter list
- Use array methods on the rest array directly; with no extras it is an empty array
- Spread an array at the call site instead of passing it into the rest parameter
- Know that a rest parameter contributes nothing to a function's fn.length
Understanding Rest parameters and variadic functions
A rest parameter turns the tail of an argument list into a value you can work with. When a call happens, arguments are matched to the named parameters from left to right, and whatever is left over is collected into a new array bound to the name after the three dots. That array is created on every call, so a function declared as sum(...nums) sees an empty array when it is called with no arguments, never undefined. This is why the empty-input test for a variadic function is nums.length === 0, and why nums.reduce works with no conversion step: nums is an ordinary Array instance with every array method already on it.
The collector has to be the last parameter, and there can be only one of them. Binding is positional, so "everything that remains" is a well-defined instruction only at the end of the list; if another parameter followed it, the engine would need an invented rule about how many arguments to hold back. Because the problem is the shape of the parameter list rather than the values passed in, engines reject it while parsing, so the file never begins running and no try/catch can intercept it. The upside of that rule is that the fixed leading parameters are bound first, so a label or separator sitting at the front never leaks into the rest array.
The three dots do opposite things depending on where they appear, and position alone decides which. In a parameter list they pack separate arguments into one array; in a call or an array literal they unpack one iterable into separate items. That symmetry produces the standard forwarding shape, where a wrapper collects with (...args) and passes the same list on with ...args, and it also explains the most common bug on this topic: handing over an already-built array where the function expected loose arguments gives you an array nested one level deeper than you wanted.
function average(label, ...nums) {
if (nums.length === 0) return `${label}: no data`;
const total = nums.reduce((sum, n) => sum + n, 0);
return `${label}: ${total / nums.length}`;
}
console.log(average('week 1', 3, 4, 5));
console.log(average('week 2'));
console.log('declared arity:', average.length);In a parameter list, ...name collects every argument left over after the named parameters into a brand-new real array, which is what makes a function variadic.
Worked examples
Why the collector must come last
An illegal parameter list is rejected while parsing, and the trailing argument is reached through the array instead.
try {
new Function('...items, last', 'return last;');
} catch (err) {
console.log('rejected at parse time:', err.name);
}
function lastOf(...items) {
return items[items.length - 1];
}
console.log(lastOf('a', 'b', 'c'));
console.log(lastOf());Example explained
Line 1new Function parses its parameter list when it is constructed, so the illegal list throws a SyntaxError you can catch instead of stopping the whole script.
Line 2Written literally in source code, the same parameter list would be an early error and none of the file would run.
Line 3items[items.length - 1] is how you reach a trailing argument once you accept that the collector has to sit at the end.
Line 4With no extra arguments items is an empty array, so the lookup is items[-1], which is undefined rather than a crash.
Packing in the parameter, unpacking at the call
Shows the difference between passing three numbers and passing one array to the same variadic function.
function loggedMax(...nums) {
console.log(nums.length, JSON.stringify(nums));
return Math.max(...nums);
}
console.log(loggedMax(4, 17, 9));
console.log(loggedMax([4, 17, 9]));Example explained
Line 1A rest parameter counts arguments, not elements, so three separate numbers become three elements.
Line 2Passing the same numbers as one array gives a one-element rest array whose only element is that array.
Line 3Math.max(...nums) inside the body spreads the collected array back into separate arguments, the exact inverse of what the parameter did.
Line 4Math.max([4, 17, 9]) coerces the array to the string 4,17,9, which is not a number, so the result is NaN rather than an error.
Rest, defaults, and declared arity
A rest parameter may follow a defaulted parameter, and neither one is counted by fn.length.
function report(title, unit = 'ms', ...samples) {
return `${title}: ${samples.length} ${unit} samples`;
}
console.log(report.length);
console.log(report('load', 'ms', 12, 15, 9));
console.log(report('idle'));Example explained
Line 1fn.length counts only the parameters before the first default or rest parameter, so it is 1 even though the function accepts any number of arguments.
Line 2The rest parameter is allowed after a defaulted one, but it still has to be final, and giving it a default of its own (...samples = []) is a SyntaxError.
Line 3In report('idle') the two fallback mechanisms act independently: unit falls back to 'ms' and samples is an empty array.
Line 4samples.length reports how many extra arguments arrived, which is the honest way to branch on variadic input.
Important notes
An arrow function needs parentheses around a rest parameter: (...args) => args.length works, while the bare single-parameter shorthand ...args => args.length is a SyntaxError.
The same packing rule appears in destructuring patterns, as in const [first, ...others] = list, where ...others collects the remaining elements rather than the remaining arguments.
Common mistakes
Calling sum(list) instead of sum(...list): the rest array becomes [list], so length is 1 and the arithmetic silently produces NaN or a concatenated string instead of a clear error.
Putting the collector before another parameter, as in function f(...args, callback), or leaving a trailing comma after it: both are parse-time SyntaxErrors, so the whole file fails to load and try/catch cannot help.
Writing args = args || [] or checking args === undefined inside a variadic function: the rest parameter is always a fresh array, so that branch is dead code and distracts from the real args.length check.
Try it yourself
Change, predict, then run
In a browser console, write range(label, ...nums) that returns label followed by the smallest and largest value joined with a dash, using Math.min and Math.max with the rest array spread, and returns label plus ": none" when no numbers are given. Call it once with loose numbers and once with a spread array to confirm both calls produce the same string.
Open the JavaScript workspaceCheck your understanding
Why does JavaScript reject function f(...items, last) {} while parsing, instead of simply giving last the final argument?
- Because arguments bind to parameters positionally, and a parameter meaning "all remaining arguments" is unambiguous only at the end of the list
- Because last would shadow items, and shadowing is not allowed between parameters
- Because rest parameters are only permitted in arrow functions, where they must come first
- Because the engine cannot build the rest array until every argument has been evaluated
Show answer
Binding walks the parameter list from left to right, so a greedy collector followed by more parameters would require an invented rule about how many arguments to hold back; only the final position makes "the rest" well defined. The evaluation-order option is tempting but wrong: all arguments are evaluated before any binding happens, and the error is raised at parse time, before a single argument exists.