Destructuring Function Parameters With Defaults
Replace long lists of positional arguments with one options object, destructured in the parameter list with defaults. Add = {} so a call with no argument doesn't crash, and know that defaults only replace undefined.
When a function takes more than two or three arguments, take one object instead and destructure it in the parameter list, with defaults. Calls become readable, argument order stops mattering, and the defaults are written where you can see them. There's one trap, calling it with no argument at all, and it has a one-character fix: = {}.
Skip ahead: Already using function f({ a, b } = {})? Skip to "Required options".
The problem with positional arguments
sendInvoice(1042, true, false, "PHP", 30);
What's true? What's 30? You have to open the function to find out, and if someone swaps two booleans, nothing complains. Adding a sixth option later means every call has to pass the first five.
The fix: one object, destructured
function sendInvoice({ orderId, email = true, includeVat = true, currency = "PHP", dueDays = 30 }) {
return `order ${orderId}: email=${email}, vat=${includeVat}, ${currency}, due in ${dueDays} days`;
}
console.log(sendInvoice({ orderId: 1042 }));
console.log(sendInvoice({ orderId: 1043, includeVat: false, dueDays: 15 }));
order 1042: email=true, vat=true, PHP, due in 30 days
order 1043: email=true, vat=false, PHP, due in 15 days
The call says what each value means. Leave out anything that should use its default. Order doesn't matter. Adding a new option with a default doesn't break any existing call.
The trap: calling with nothing
function sendInvoice({ orderId, email = true }) {
return `order ${orderId}, email=${email}`;
}
try {
sendInvoice();
} catch (error) {
console.log(`${error.name}: ${error.message}`);
}
TypeError: Cannot destructure property 'orderId' of 'undefined' as it is undefined.
The defaults are for properties inside the object. When there's no object, there's nothing to destructure. Give the whole parameter a default too:
function listOrders({ status = "all", limit = 20 } = {}) {
return `status=${status}, limit=${limit}`;
}
console.log(listOrders());
console.log(listOrders({ limit: 5 }));
status=all, limit=20
status=all, limit=5
= {} at the end means "if no object was passed, use an empty one". Then every property falls back to its own default. Add it whenever every option has a default.
Defaults only replace undefined
function listOrders({ status = "all", limit = 20 } = {}) {
return `status=${status}, limit=${limit}`;
}
console.log(listOrders({ limit: null }));
console.log(listOrders({ limit: undefined }));
console.log(listOrders({ limit: 0 }));
status=all, limit=null
status=all, limit=20
status=all, limit=0
null and 0 are real values, so they're kept. That's usually right for 0. For null, it bites when data comes from JSON or a database, where "no value" is often null. If null should also mean "use the default", handle it in the body: const pageSize = limit ?? 20;.
⚠ Warning: Don't use || for defaults of numbers or booleans: const limit = options.limit || 20 turns a deliberate 0 into 20, and options.email || true can never be false. Use destructuring defaults, or ??, which only replaces null and undefined.
Required options
Defaults are for optional values. For a required one, fail early with a clear message instead of letting undefined travel:
function required(name) {
throw new TypeError(`${name} is required`);
}
function sendInvoice({ orderId = required("orderId"), currency = "PHP" } = {}) {
return `order ${orderId} in ${currency}`;
}
console.log(sendInvoice({ orderId: 1042 }));
try {
sendInvoice({ currency: "USD" });
} catch (error) {
console.log(error.message);
}
order 1042 in PHP
orderId is required
A default expression only runs when the value is missing, so required() throws exactly then. Some people find this too clever. A plain if (orderId === undefined) throw ... on the first line of the function does the same job. Pick one and use it consistently.
Renaming and nested options
function formatPrice({ amount, currency: code = "PHP", format: { decimals = 2 } = {} } = {}) {
return `${code} ${amount.toFixed(decimals)}`;
}
console.log(formatPrice({ amount: 425 }));
console.log(formatPrice({ amount: 425, currency: "USD", format: { decimals: 0 } }));
PHP 425.00
USD 425
currency: code reads currency into a variable called code. format: { decimals = 2 } = {} reaches into a nested object, with its own = {} so a missing format doesn't throw.
💡 Tip: Stop at one level of nesting in the parameter list. Beyond that, the signature is harder to read than the function. Destructure the rest in the first lines of the body.
Common mistakes
- Missing
= {}, so a call with no argument throws. - Expecting a default to replace
null. - Passing positional arguments to a function that now expects an object:
sendInvoice(1042)destructuresorderIdfrom the number 1042 and getsundefined. - Renaming syntax backwards: it's
{ original: newName }.
Look at a function you call with three or more arguments. What would the call read like with an options object?
About
Turn positional arguments into an options object with defaults, avoid the crash on an empty call, and handle null, required options and nested options. After this, your function calls say what each value means.
- Responsible
- John G
- Last Update
- 09/01/2026
- Reading Time
- 4 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.