How to correctly specify default options in ES6

SHARE ON

When writing modern JavaScript, it’s common to collect multiple optional parametersinto a trailing “options” object. This allows clients to pass a subset of options
and gives context to arguments at the call site. It also permits the API to
use ES6 default parameters. But it can be tricky to get default parameters right
with objects. Here’s how.

Options objects

Once upon a time, if you wanted to dispatch a mouse event, you had to call an
API that looked like this:

function initMouseEvent(type, canBubble, cancelable, view,
  detail, screenX, screenY, clientX, clientY,
  ctrlKey, altKey, shiftKey, metaKey,
  button, relatedTarget) {}

There were sensible defaults for every parameter except type, but the API was
not able to express this. Specifying a value for relatedTarget meant specifying
values for every other parameter, leading to insanely long, inscrutable function
calls like:

let evt1 = document.createEvent('MouseEvents');
evt.initMouseEvent('mouseup', true, true, window, 1, mouseX, mouseY, 0, 0, false, false, false, false, 0, el);

This was fixed by collecting the optional parameters into a trailing object:

function MouseEvent(type, mouseEventInit) {}

With this sort of API, the client can specify a subset of options. And as a bonus,
the options are named at the call site, kind of like Python’s named parameters:

let evt2 = new MouseEvent('mouseup', { relatedTarget: el });

Default parameters

Another downside of APIs like initMouseEvent is that they can’t effectively use
ES6 default parameters. This is because parameters are set left-to-right, so
multiple default parameters are rarely practical:

function f(x = 1, y = 2) {
  return [x, y];
}

// Specifying neither x nor y:
f();  // [1, 2]

// Specifying x but not y
f(2); // [2, 2];

// But now there's no way to specify y and not x.

With an options object, you can write

function f(options = { x: 1, y: 2 }) {
  return [options.x, options.y];
}

However, this API does not express that x and y are independent options.
Instead, it forces x and y to be passed together:

f(); // [1, 2]
f({ x: 1 }) // [1, undefined]

The problem is that options is defined wholesale. The way to fix this is to
use another ES6 feature, destructuring assignment, to describe the structure
of options. You’ll no longer be able to reference options per se, but it’s often nicer to get
“direct” access to each key, and then you can specify the defaults on a per-key basis:

function f({ x = 1, y = 2 }) {
  return [x, y];
}

f({ x: 1 }); // [1, 2]

But there’s one final fix to be made. The previous version of the function has
forced the client to pass an object:

f(); // Uncaught TypeError: Cannot destructure property `x` of 'undefined' or 'null'.

The way to fix this is to default the options object to an empty object.

function f({ x = 1, y = 2 } = {}) {
  return [x, y];
}

f(); // [1, 2]

Conclusion

Default parameters are an awesome ES6 feature. However, the ability to specify objects as
default values is a clear footgun. You should always
specify default options by destructuring, while making sure that the options object is defined by
defaulting to an empty object.

Working at the cutting edge of JavaScript development means getting used to
some new syntax. If you’d like to join us, drop us a line at careers@mixmax.com.

SHARE ON

Written By

Jeff Wear

Jeff Wear

From Your Friends At