This blog post describes the ECMAScript proposal “Optional chaining” by Gabriel Isenberg, Claude Pache, and Dustin Savery.
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:
undefined nor null, then perform the operation after the question mark.undefined.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]
);
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)']
);
The remaining sections cover advanced aspects of optional chaining.
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);
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);
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.
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 && ba || bc ? t : eUntil 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.
&& 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.
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);
}
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')
@babel/plugin-proposal-optional-chaining.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) : []
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.