Default arguments in ES6, put simply: I'm a huge fan.
I wanted to document a few of the ways I use default args and how they've made my old ES5 workarounds go away in favour of a much more elegant code design.
UK EVENTAttend ffconf.org 2024
The conference for people who are passionate about the web. 8 amazing speakers with real human interaction and content you can't just read in a blog post or watch on a tiktok!
£249+VAT - reserve your place today
The backets-or
Here's my original function. It takes an argument for the DOM node to append a child node to, but if it's omitted (because target
is optional), then it'll be inserted into the body. I've wrapped the optional element in bracket, and used ||
(or) to use the body element if target
is undefined.
function show(target) {
const canvas = document.createElement('canvas');
(target || document.body).appendChild(canvas);
// <snipped-code>
}
Instead with default args, much more elegant:
function show(target = document.body) {
const canvas = document.createElement('canvas');
target.appendChild(canvas);
// <snipped-code>
}
Elegant because it immediately shows:
- the reader what the default is if
target
is omitted. target
can be omitted.- what type the target should be.
- avoids the ugly
(x || default)
statement which could look a bit weird anyway.
Defaults from other arguments
When combining defaults with defaults that (optionally) rely on another argument, the code can get unnecessarily hairy (this example is rather tame to some of my older code!):
function crop(source, width, height) {
if (width === undefined) {
width = 200;
}
if (height === undefined) {
height = width;
}
// => 200, 200 when called with `crop()`
console.log(width, height);
// <snipped-cropping-code>
}
By using the argument defaults, the width is easily defaulted to 200
, and since the width
is defined first, the height
argument can rely on the width
argument. This makes for quite a nice pattern, so long as you're careful about the order that you declare the variables:
function crop(source, width = 200, height = width) {
console.log(width, height); // => 200, 200
// <snipped-cropping-code>
}
Argument validation
Argument defaults can also be used for some enforced argument validation, although it only checks for presence rather than correct types (which may arguably have more use).
function required(what) {
throw new Error(`Param ${what} is required`);
}
function createUser({
username = required('username'),
password = required('password'),
}) {
// <snipped-sign-in>
}
Although this same code could go inside the function, it does leave the argument checking in the place that arguments are slurped up, and leaves the function body for doing only what it intends to do.
Note that I've also used object destructuring in my createUser
function, which means that the order of the arguments is no longer important - which I personally think is pretty useful when there's a number of arguments all pertaining to the same model.
Initialising during destructuring
This one isn't technically a default argument…I don't think, but it looks awfully similar, and it's useful in the same manner. Below I have a blogPost
object with a number of properties and status
is optional and if omitted needs to be set to DRAFT
. Using this syntax makes the default easily readable:
const { title, description, status = 'DRAFT' } = blogPost;
Overboard…
You can of course go utterly overboard, and I'd consider the following change an anti-pattern:
function imageToPixels(img, scale) {
const ctx = imageToCanvas(img, scale);
return ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height);
}
Changed to:
const imageToPixels = (img, scale, ctx = imageToCanvas(img, scale)) =>
ctx.getImageData(0, 0, ctx.canvas.width, ctx.canvas.height));
What inspired this post
I've been meaning to write up some of my usage of argument defaults as I remember having them in my Perl days (late 90s, early 2000s) and always missed them as I used JavaScript.
Just today I wrote the following line of code:
let [{ ink: paper }, { ink } = { ink: 0 }] = Array.from(inks)
.map((count, ink) => ({ ink, count }))
.filter(({ count }) => count)
.sort((a, b) => a.count - b.count)
.slice(-2);
This code does a number of things, only to then utterly exploit defaults and destructuring! A quick summary: turn inks
from a UInt8Array
into an Array
so when I map
it doesn't get cast back into a uint_8
. I'm mapping the array into a two property array (ink
is the value I need, and count
is the occurrences). Then filter and sort by those having a count, and slice
to return an array whose length is 2 (or less).
The resulting array is capture and destructured into two objects, that are also destructured collecting the ink
property. The first is renamed to paper
(which would be the 2nd most popular ink colour) and the second item is captured as ink, but before we're done, it's possible the slice(-2)
returned an array length of 1, so I need to provide a default which sets the ink
value to 0. Fancy. Possibly a bit obtuse. But I like it :-)