ES2020: Nullish coalescing for JavaScript

[2019-08-19] dev, javascript, es feature, es2020
(Ad, please don’t block)

This blog post describes the ECMAScript feature proposal “Nullish coalescing for JavaScript” by Gabriel Isenberg. It proposes the ?? operator that replaces || when it comes to providing default values.

Overview  

The result of actualValue ?? defaultValue is:

  • defaultValue if actualValue is undefined or null
  • actualValue otherwise

Examples:

> undefined ?? 'default'
'default'
> null ?? 'default'
'default'

> false ?? 'default'
false
> '' ?? 'default'
''
> 0 ?? 'default'
0

Legacy: default values via ||  

Quick recap of how the logical Or operator || works – the following two expressions are equivalent:

a || b
a ? a : b

That is:

  • If a is truthy then the result of a || b is a.
  • Otherwise it is b.

That makes it possible to use || to specify a default value that is to be used if the actual value is falsy:

const result = actualValue || defaultValue;
function getTitle(fileDesc) {
  return fileDesc.title || '(Untitled)';
}
const files = [
  {path: 'index.html', title: 'Home'},
  {path: 'tmp.html'},
];
assert.deepEqual(
  files.map(f => getTitle(f)),
  ['Home', '(Untitled)']);

Note that almost always, the default value should only be used if the actual value is undefined or null. That works, because both undefined and null are falsy:

> undefined || 'default'
'default'
> null || 'default'
'default'

Alas, the default value is also used if the actual value is any of the other falsy values – for example:

> false || 'default'
'default'
> '' || 'default'
'default'
> 0 || 'default'
'default'

Therefore, this invocation of getTitle() does not work properly:

assert.equal(
  getTitle({path: 'empty.html', title: ''}),
  '(Untitled)');

Nullish coalescing operator ??  

The nullish coalescing operator ?? is intended to replace the logical Or operator || when it comes to providing default values. The following two expressions are equivalent:

a ?? b
a !== undefined && a !== null ? a : b

Default values are provided like this:

const result = actualValue ?? defaultValue;

For undefined and null, the ?? operator works the same as the || operator:

> undefined ?? 'default'
'default'
> null ?? 'default'
'default'

However, for other left-hand-side operands, it never returns the default values – even if the operands are falsy:

> false ?? 'default'
false
> '' ?? 'default'
''
> 0 ?? 'default'
0

Let’s rewrite getTitle() and use ??:

function getTitle(fileDesc) {
  return fileDesc.title ?? '(Untitled)';
}

Now calling it with a fileDesc whose .title is the empty string, works as desired:

assert.equal(
  getTitle({path: 'empty.html', title: ''}),
  '');

Default values via destructuring  

getTitle() was used as a simple way of motivating the ?? operator. Note that you can also use destructuring to implement it:

function getTitle({title = '(Untitled)'}) {
  return title;
}

A realistic example of using the ?? operator  

As a realistic example, we will use the ?? to simplify the following function. The string method .match() is explained in “JavaScript for impatient programmers”.

function countMatches(regex, str) {
  if (!regex.global) {
    throw new Error(
      'Regular expression must have flag /g: ' + regex);
  }
  const matchResult = str.match(regex); // null or Array
  if (matchResult === null) {
    return 0;
  } else {
    return matchResult.length;
  }
}

assert.equal(
  countMatches(/a/g, 'ababa'), 3);
assert.equal(
  countMatches(/b/g, 'ababa'), 2);
assert.equal(
  countMatches(/x/g, 'ababa'), 0);

// Flag /g is missing
assert.throws(
  () => countMatches(/a/, 'ababa'), Error);

If we use the ?? operator, we get the following code:

function countMatches(regex, str) {
  if (!regex.global) {
    throw new Error(
      'Regular expression must have flag /g: ' + regex);
  }
  return (str.match(regex) ?? []).length;
}

Nullish coalescing and optional chaining  

The nullish coalescing operator ?? was explicitly designed to complement optional chaining of property accesses. For example, in the following code, the two are used together in line A.

const persons = [
  {
    surname: 'Zoe',
    address: {
      street: {
        name: 'Sesame Street',
        number: '123',
      },
    },
  },
  {
    surname: 'Mariner',
  },
  {
    surname: 'Carmen',
    address: {
    },
  },
];

const streetNames = persons.map(
  p => p.address?.street?.name ?? '(no name)'); // (A)
assert.deepEqual(
  streetNames, ['Sesame Street', '(no name)', '(no name)']
);

Implementations  

Further reading