ES2020: optional chaining

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

This blog post describes the ECMAScript proposal “Optional chaining” by Gabriel Isenberg, Claude Pache, and Dustin Savery.

Overview  

The following kinds of optional operations exist.

obj?.prop     // optional static property access
obj?.[«expr»] // optional dynamic property access
func?.(«arg0», «arg1») // optional function or method call

The rough idea is:

  • If the value before the question mark is neither undefined nor null, then perform the operation after the question mark.
  • Otherwise, return undefined.

Example: optional static property access  

Consider the following data:

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

We can use optional chaining to safely extract street names:

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

Handling defaults via nullish coalescing  

The proposed nullish coalescing operator allows us to use the default value '(no street)' instead of undefined:

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

(Advanced)  

The remaining sections cover advanced aspects of optional chaining.

The operators in more detail  

Optional static property access  

The following two expressions are equivalent:

o?.prop
(o !== undefined && o !== null) ? o.prop : undefined

Examples:

assert.equal(undefined?.prop, undefined);
assert.equal(null?.prop,      undefined);
assert.equal({prop:1}?.prop,  1);

Optional dynamic property access  

The following two expressions are equivalent:

o?.[«expr»]
(o !== undefined && o !== null) ? o[«expr»] : undefined

Examples:

const key = 'prop';
assert.equal(undefined?.[key], undefined);
assert.equal(null?.[key], undefined);
assert.equal({prop:1}?.[key], 1);

Optional function or method call  

The following two expressions are equivalent:

f?.(arg0, arg1)
(f !== undefined && f !== null) ? f(arg0, arg1) : undefined

Examples:

assert.equal(undefined?.(123), undefined);
assert.equal(null?.(123), undefined);
assert.equal(String?.(123), '123');

Note that this operator produces an error if its left-hand side is not callable:

assert.throws(
  () => true?.(123),
  TypeError);

Why? The idea is that the operator only tolerates deliberate omissions. An uncallable value (other than undefined and null) is probably an error and should be reported, rather than worked around.

Short-circuiting  

In a chain of property accesses and function/method invocations, evaluation stops once the first optional operator encounters undefined or null at its left-hand side:

function isInvoked(obj) {
  let invoked = false;
  obj?.a.b.m(invoked = true);
  return invoked;
}

assert.equal(
  isInvoked({a: {b: {m() {}}}}), true);
  
// The left-hand side of ?. is undefined
// and the assignment is not executed
assert.equal(
  isInvoked(undefined), false);

This behavior differs from a normal operator/function where JavaScript always evaluates all operands/arguments before evaluating the operator/function. It is called short-circuiting. Other short-circuiting operators:

  • a && b
  • a || b
  • c ? t : e

Alternatives to optional chaining  

Until now, the following alternatives to optional chaining were used in JavaScript.

&& operator  

The following two expressions are roughly equivalent:

p.address?.street?.name
p.address && p.address.street && p.address.street.name

For each a && b, b is only evaluated (and returned) if a is truthy. a therefore acts as a condition or guard for b.

The downsides of &&  

Apart from the verbosity, using && has two downsides.

First, if it fails, && returns its left-hand side, while ?. always returns undefined:

const value = null;
assert.equal(value && value.prop, null);
assert.equal(value?.prop, undefined);

Second, && fails for all falsy left-hand sides, while ?. only fails for undefined and null:

const value = '';
assert.equal(value?.length, 0);
assert.equal(value && value.length, '');

Note that, here, && returning its left-hand side is worse than in the previous example.

Destructuring  

In principle, you can also use destructuring for handling chained property accesses. But it’s not pretty:

for (const p of persons) {
  const { address: { street: {name=undefined}={} }={} } = p;
  assert.equal(
    name,
    p.address?.street?.name);
}

Lodash get()  

The function get() of the Lodash library is another alternative to optional chaining.

For example, the following two expressions are equivalent:

import {get} from 'lodash-es';

p.address?.street?.name
get(p, 'address.street.name')

Availability of optional chaining  

Frequently asked questions  

Why are there dots in o?.[x] and f?.()?  

The syntaxes of the following two optional operator are not ideal:

obj?.[«expr»]          // better: obj?[«expr»]
func?.(«arg0», «arg1») // better: func?(«arg0», «arg1»)

Alas, the less elegant syntax is necessary, because distinguishing the ideal syntax (first expression) from the conditional operator (second expression) is too complicated:

obj?['a', 'b', 'c'].map(x => x+x)
obj ? ['a', 'b', 'c'].map(x => x+x) : []

Why does null?.prop evaluate to undefined and not null?  

The operator ?. is mainly about its right-hand side: Does property .prop exist? If not, stop early. Therefore, keeping information about its left-hand side is rarely useful. However, only having a single “early termination” value does simplify things.