Q1With const addFee = (price, fee = 500) => price + fee;, what does addFee(1000, undefined) return?
Default and Rest Parameters — Fallback Values and Collecting the Rest
Covers default parameters, rest parameters that collect arguments into an array, and the options object pattern.
Some arguments get the same value on almost every call, like a shipping fee or a tax rate. Leave one out and there's no error; you only notice it's missing when undefined shows up in the output.
This article covers default parameters, which set the value used for a left-out argument, rest parameters, which collect any number of arguments, and the options pattern, which passes settings as a single object.
Making Arguments Optional — Default Parameters
Say you have a function that builds a meeting room booking, where the duration is almost always 1 hour and the layout is almost always classroom style. If you still have to write all three arguments on every call, the one value you actually want to change gets lost in the call.
A default parameter (a way of setting in advance the value to use when an argument isn't passed) is written by adding = in the parameter list, as in hours = 1. A parameter with a default can be left out at the call site.
// Write a default by adding "= value" in the parameter list
const reserveRoom = (roomName, hours = 1, layout = "classroom") => {
return `${roomName} / ${hours} hr / ${layout}`;
};
// If you leave out the 3rd, the default "classroom" is used
console.log(reserveRoom("Room A", 2)); // Room A / 2 hr / classroom
// When you pass a value, the default isn't used
console.log(reserveRoom("Room A", 2, "boardroom")); // Room A / 2 hr / boardroom
// If you also leave out the 2nd, both defaults are used
console.log(reserveRoom("Room A")); // Room A / 1 hr / classroom
reserveRoom("Room A", 2) one parameter position at a time. Even with a default, it doesn't kick in when a value is passed.You can only leave out arguments from the right, so put parameters with defaults last. Even if you only want to change the 3rd argument, you still have to write a value in the 2nd position.
What a Missing Argument Gets — undefined and Defaults
If you leave out an argument for a parameter with no default, any calculation or string that uses it carries on anyway. Since nothing stops, you end up hunting afterward for the call that forgot to pass it.
A parameter that isn't passed an argument gets undefined. The default is used only when the argument is undefined — if you pass null, 0, or an empty string, the value you passed is used.
const notifyStatus = (orderId, status = "Received") => `${orderId}: ${status}`;
// Leaving it out and passing undefined both give you the default
console.log(notifyStatus("A-1021")); // A-1021: Received
console.log(notifyStatus("A-1021", undefined)); // A-1021: Received
// null counts as a passed value, so the default isn't used
console.log(notifyStatus("A-1021", null)); // A-1021: null
// A left-out parameter with no default gets undefined
const plainStatus = (orderId, status) => `${orderId}: ${status}`;
console.log(plainStatus("A-1021")); // A-1021: undefined
status gets undefined, so the default expression runs. null is treated as a passed value, so the default doesn't run.The table below shows what ends up in status, and what gets displayed, for each second argument passed to notifyStatus. Passing undefined gives the same result as leaving the argument out, while null, just like a string, is displayed as passed.
| Call | Value in status | Displayed |
|---|---|---|
| notifyStatus("A-1021") | The default, Received | A-1021: Received |
| notifyStatus("A-1021", "Shipped") | Shipped | A-1021: Shipped |
| notifyStatus("A-1021", undefined) | The default, Received | A-1021: Received |
| notifyStatus("A-1021", null) | null | A-1021: null |
Passing null Skips the Default
null is a value meaning "intentionally empty," so a default in the parameter list won't replace it. If you want the fallback even when an API response gives you null, skip the parameter default and use ?? inside the function instead, as in const shown = status ?? "Received";.
Collecting Any Number of Arguments — Rest Parameters
Some functions receive a different number of values on every call, like the tags on a product or the line items on an expense report. Even if you declare five parameters, some call will eventually need a sixth, and the unused ones are undefined every time.
A rest parameter (a way of receiving the remaining arguments bundled into a single array; also called variadic arguments) is written as ...labels at the end of the parameter list. It's the same ... that collected the remaining elements in destructuring, except that here it collects arguments. It can only be the last parameter; putting another parameter after it is a SyntaxError.
// Writing ...labels bundles the arguments received into a single array
const buildTags = (...labels) => `Tags (${labels.length}): ${labels.join(" / ")}`;
console.log(buildTags("Free Shipping", "In Stock")); // Tags (2): Free Shipping / In Stock
console.log(buildTags("New")); // Tags (1): New
// Going the other way, ... inside the call's parentheses turns an array back into arguments
console.log(buildTags(...["New", "Limited"])); // Tags (2): New / Limited
// When there are also named parameters, put the rest parameter last
const buildNotice = (orderId, ...notes) => `${orderId} (${notes.join(", ")})`;
console.log(buildNotice("A-1021", "Chilled", "Delivery window")); // A-1021 (Chilled, Delivery window)
console.log(buildNotice("A-1022")); // A-1022 ()
orderId, and the rest go into the elements of notes, in order. A rest parameter receives whatever's left after the named parameters.A rest parameter can't have a default, but when no arguments are passed for it, notes is an empty array, not undefined. Going the other way, if you have an array and want to pass its items as individual arguments, write ... inside the call's parentheses.
Passing Settings by Name — Options Objects and Destructuring
When settings pile up, as with sending an email, you end up with calls like sendMail(to, subject, false, true). You can't tell what that false and true mean without opening the function's definition.
With the options pattern (a way of passing several settings bundled into a single object), the receiving side destructures the object and gives each key a default. To handle calls that pass no settings at all, add = {} after the destructuring pattern so an empty object is used when no object is passed.
// The more parameters there are, the harder it is to tell what a call is passing
const sendMailOld = (to, subject, isUrgent, withPdf) =>
`${to} / ${subject} / ${isUrgent} / ${withPdf}`;
console.log(sendMailOld("alice@example.com", "Shipping notice", false, true));
// alice@example.com / Shipping notice / false / true
// options pattern: bundle the settings into one object and destructure it on the receiving side
const sendMail = (to, { subject = "Notice", isUrgent = false } = {}) =>
`${to} / ${subject} / ${isUrgent}`;
console.log(sendMail("alice@example.com", { isUrgent: true })); // alice@example.com / Notice / true
console.log(sendMail("alice@example.com")); // alice@example.com / Notice / false
Instead of depending on parameter order, each value is labeled by its key, so calls stay readable even as settings grow. Keys are matched by name, so even if you write them in a different order, as in { isUrgent: true, subject: "Resend" }, the values received are the same.
Leaving Out = {} Causes a TypeError
If you write only the destructuring pattern, as in { subject = "Notice" }, a call that leaves out the second argument throws TypeError: Cannot read properties of undefined (reading 'subject'). That's because it tries to read a key from undefined, which is what the left-out parameter holds.
Knowledge Check
Answer each question one by one.
Q2What does const countNotes = (id, ...notes) => notes.length; return when called as countNotes("A-1021")?
Q3What does const send = (to, { urgent = false } = {}) => urgent; return when called as send("alice@example.com")?