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 && b
a || b
c ? t : e
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
.
&&
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.