How to correctly specify default options in ES6

Default object values considered harmful

Wednesday, Jan 24th, 2018

Mixmax is a communications platform that brings professional communication & email into the 21st century.

When writing modern JavaScript, it's common to collect multiple optional parameters into 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.