Optional Chaining for JavaScript
Status
ECMAScript proposal at stage 4 of the process.
Authors
- Claude Pache (github)
- Gabriel Isenberg (github, twitter)
- Daniel Rosenwasser (github, twitter)
- Dustin Savery (github, twitter)
Overview and motivation
When looking for a property value that's deep in a tree-like structure, one often has to check whether intermediate nodes exist:
var street = user.address && user.address.street;
Also, many API return either an object or null/undefined, and one may want to extract a property from the result only when it is not null:
var fooInput = myForm.querySelector('input[name=foo]')
var fooValue = fooInput ? fooInput.value : undefined
The Optional Chaining Operator allows a developer to handle many of those cases without repeating themselves and/or assigning intermediate results in temporary variables:
var street = user.address?.street
var fooValue = myForm.querySelector('input[name=foo]')?.value
When some other value than undefined
is desired for the missing case, this can usually be handled with the Nullish coalescing operator:
// falls back to a default value when response.settings is missing or nullish
// (response.settings == null) or when response.settings.animationDuration is missing
// or nullish (response.settings.animationDuration == null)
const animationDuration = response.settings?.animationDuration ?? 300;
The call variant of Optional Chaining is useful for dealing with interfaces that have optional methods:
iterator.return?.() // manually close an iterator
or with methods not universally implemented:
if (myForm.checkValidity?.() === false) { // skip the test in older web browsers
// form validation fails
return;
}
Prior Art
Unless otherwise noted, in the following languages, the syntax consists of a question mark prepending the operator, (a?.b
, a?.b()
, a?[b]
or a?(b)
when applicable).
The following languages implement the operator with the same general semantics as this proposal (i.e., 1) guarding against a null base value, and 2) short-circuiting application to the whole chain):
- C#: Null-conditional operator โ null-conditional member access or index, in read access.
- Swift: Optional Chaining โ optional property, method, or subscript call, in read and write access.
- CoffeeScript: Existential operator โ existential operator variant for property accessor, function call, object construction (
new a?()
). Also applies to assignment and deletion.
The following languages have a similar feature, but do not short-circuit the whole chain when it is longer than one element. This is justified by the fact that, in those languages, methods or properties might be legitimately used on null (e.g., null.toString() == "null" in Dart):
- Kotlin: Safe calls โ optional property access for read; optional property assignment for write.
- Dart: Conditional member access โ optional property access.
- Ruby: Safe navigation operator โ Spelled as:
a&.b
The following languages have a similar feature. We havenโt checked whether they have significant differences in semantics with this proposal:
- Groovy: Safe navigation operator
- Angular: Safe navigation operator (link to archived documentation for Angular v10)
Syntax
The Optional Chaining operator is spelled ?.
. It may appear in three positions:
obj?.prop // optional static property access
obj?.[expr] // optional dynamic property access
func?.(...args) // optional function or method call
Notes
- In order to allow
foo?.3:0
to be parsed asfoo ? .3 : 0
(as required for backward compatibility), a simple lookahead is added at the level of the lexical grammar, so that the sequence of characters?.
is not interpreted as a single token in that situation (the?.
token must not be immediately followed by a decimal digit).
Semantics
Base case
If the operand at the left-hand side of the ?.
operator evaluates to undefined or null, the expression evaluates to undefined. Otherwise the targeted property access, method or function call is triggered normally.
Here are basic examples, each one followed by its desugaring. (The desugaring is not exact in the sense that the LHS should be evaluated only once and that document.all
should behave as an object.)
a?.b ย ย ย ย // undefined if `a` is null/undefined, `a.b` otherwise.
a == null ? undefined : a.b
a?.[x] ย ย ย // undefined if `a` is null/undefined, `a[x]` otherwise.
a == null ? undefined : a[x]
a?.b() ย ย ย // undefined if `a` is null/undefined
a == null ? undefined : a.b() // throws a TypeError if `a.b` is not a function
ย ย ย ย ย ย ย ย ย ย ย ย ย ย // otherwise, evaluates to `a.b()`
a?.() ย ย ย // undefined if `a` is null/undefined
a == null ? undefined : a() // throws a TypeError if `a` is neither null/undefined, nor a function
ย ย ย ย ย ย // invokes the function `a` otherwise
Short-circuiting
If the expression on the LHS of ?.
evaluates to null/undefined, the RHS is not evaluated. This concept is called short-circuiting.
a?.[++x] ย ย ย ย // `x` is incremented if and only if `a` is not null/undefined
a == null ? undefined : a[++x]
Long short-circuiting
In fact, short-circuiting, when triggered, skips not only the current property access, method or function call, but also the whole chain of property accesses, method or function calls directly following the Optional Chaining operator.
a?.b.c(++x).d ย // if `a` is null/undefined, evaluates to undefined. Variable `x` is not incremented.
ย ย ย ย ย ย ย // otherwise, evaluates to `a.b.c(++x).d`.
a == null ? undefined : a.b.c(++x).d
Note that the check for nullity is made on a
only. If, for example, a
is not null, but a.b
is null, a TypeError will be thrown when attempting to access the property "c"
of a.b
.
This feature is implemented by, e.g., C# and CoffeeScript; see Prior Art.
Stacking
Letโs call Optional Chain an Optional Chaining operator followed by a chain of property accesses, method or function calls.
An Optional Chain may be followed by another Optional Chain.
a?.b[3].c?.(x).d
a == null ? undefined : a.b[3].c == null ? undefined : a.b[3].c(x).d
// (as always, except that `a` and `a.b[3].c` are evaluated only once)
Edge case: grouping
Parentheses limit the scope of short-circuiting:
(a?.b).c
(a == null ? undefined : a.b).c
That follows from the design choice of specifying the scope of short-circuiting by syntax (like the &&
operator), rather than propagation of a Completion (like the break
instruction) or an adhoc Reference (like an earlier version of this proposal). In general, syntax cannot be arbitrarily split by parentheses: for example, ({x}) = y
is not destructuring assignment, but an attempt to assign a value to an object literal.
Note that, whatever the semantics are, there is no practical reason to use parentheses in that position anyway.
Optional deletion
Because the delete
operator is very liberal in what it accepts, we have that feature for free:
delete a?.b
a == null ? true : delete a.b
where true
is the usual result of attempting to delete a non-Reference.
Why do we support optional deletion?
-
laziness (this argument is placed first, not because it is the most important, but because it puts the other ones in the right perspective). Laziness (together with impatience and hubris) is one of the most important virtues of the spec writer. That is, all other things being almost equal, take the solution that involves less stuff to be incorporated in the spec.
Now, it happens that supporting optional deletion requires literally zero effort, while not supporting it (by making the construct an early syntax error) requires some nontrivial effort. (See PR #73 (comment) for technical details.)
Thus, per the laziness principle, the right question is not: โAre there good reasons to support optional deletion?โ, but rather: โAre there good reasons to remove support for optional deletion?โ
-
lack of strong reason for removing support. The supported semantics of optional deletion is the only one that could be expected (provided that the semantics of the delete operator is correctly understood, of course). It is not like it could in some manner confuse the programmer. In fact, the only real reason is: โWe didnโt intend to support it.โ
-
consistency of the delete operator. It is a fact of life that this operator is very liberal in what it accepts, even pretending to succeed when it is given something that does not make sense (e.g.,
delete foo()
). The only error conditions (early or not) are in strict mode, when attempting to delete something that is barred from deletion (nonconfigurable property, variable, ...). Supporting optional deletion fits well in that model, while forbidding it does not. -
existence of use cases. Although they are not common, they do exist in practice (example from Babel).
Not supported
Although they could be included for completeness, the following are not supported due to lack of real-world use cases or other compelling reasons; see Issue # 22 and Issue #54 for discussion:
- optional construction:
new a?.()
- optional template literal:
a?.`string`
- constructor or template literals in/after an Optional Chain:
new a?.b()
,a?.b`string`
The following is not supported, although it has some use cases; see Issue #18 for discussion:
- optional property assignment:
a?.b = c
The following are not supported, as it does not make much sense, at least in practice; see Issue #4 (comment):
- optional super:
super?.()
,super?.foo
- anything that resembles to property access or function call, but is not:
new?.target
,import?.('foo')
, etc.
All the above cases will be forbidden by the grammar or by static semantics so that support might be added later.
Out of scope
There has been various interesting ideas for applying the idea of โoptionalโ to other constructs. However, there are not part of this proposal. For example:
Open issues
Private class fields and methods
Issue #28: Should optional chains support the upcoming private class fields and private methods, as in a?.#b
, a?.#b()
or a?.b.#c
? Quoting microsoft/TypeScript#30167 (comment):
This one isn't baked into the proposal yet, simply because private fields themselves aren't baked yet. So we don't want to hold up this proposal if that one happens to stall out. Once that one has reached Stage 4, we will address it then.
FAQ
obj?.[expr]
andfunc?.(arg)
look ugly. Why not useobj?[expr]
andfunc?(arg)
as does <language X>?-
We donโt use the
obj?[expr]
andfunc?(arg)
syntax, because of the difficulty for the parser to efficiently distinguish those forms from the conditional operator, e.g.,obj?[expr].filter(fun):0
andfunc?(x - 2) + 3 :1
.Alternative syntaxes for those two cases each have their own flaws; and deciding which one looks the least bad is mostly a question of personal taste. Here is how we made our choice:
- pick the best syntax for the
obj?.prop
case, which is expected to occur most often; - extend the use of the recognisable
?.
sequence of characters to other cases:obj?.[expr]
,func?.(arg)
.
As for <language X>, it has different syntactical constraints than JavaScript because of <some construct not supported by X or working differently in X>.
- pick the best syntax for the
- Ok, but I really think that <alternative syntax> is better.
-
Various alternative syntaxes has been explored and extensively discussed in the past. None of them gained consensus. Search for issues with label โalternative syntaxโ, as well as issues with label โalternative syntax and semanticsโ for those that had impact on semantics.
- Why does
(null)?.b
evaluate toundefined
rather thannull
? -
Neither
a.b
nora?.b
is intended to preserve arbitrary information on the base objecta
, but only to give information about the property"b"
of that object. If a property"b"
is absent froma
, this is reflected bya.b === undefined
anda?.b === undefined
.In particular, the value
null
is considered to have no properties; therefore,(null)?.b
is undefined. - Why does
foo?.()
throw when foo is neither nullish nor callable? -
Imagine a library which will call a handler function, e.g.
onChange
, just when the user has provided it. If the user provides the number3
instead of a function, the library will likely want to throw and inform the user of their mistaken usage. This is exactly what the proposed semantics foronChange?.()
achieve.Moreover, this ensures that
?.
has a consistent meaning in all cases. Instead of making calls a special case where we checktypeof foo === 'function'
, we simply checkfoo == null
across the board.Finally, remember that optional chaining is not an error-suppression mechanism.
- Why do you want long short-circuiting?
-
See Issue #3 (comment).
- In
a?.b.c
, ifa.b
isnull
, thena.b.c
will evaluate toundefined
, right? -
No. It will throw a TypeError when attempting to fetch the property
"c"
ofa.b
.The opportunity of short-circuiting happens only at one time, just after having evaluated the LHS of the Optional Chaining operator. If the result of that check is negative, evaluation proceeds normally.
In other words, the
?.
operator has an effect only at the very moment it is evaluated. It does not change the semantics of subsequent property accesses, method or function calls. - In a deeply nested chain like
a?.b?.c
, why should I write?.
at each level? Should I not be able to write the operator only once for the whole chain? -
By design, we want the developer to be able to mark each place that they expect to be null/undefined, and only those. Indeed, we believe that an unexpected null/undefined value, being a symptom of a probable bug, should be reported as a TypeError rather than swept under the rug.
- ...but, in the case of a deeply nested chain, we almost always want to test for
null
/undefined
at each level, no? -
Deeply nested tree-like structures is not the sole use case of Optional Chaining.
See also Usage statistics on optional chaining in CoffeeScript and compare โTotal soak operationsโ with โTotal soak operations chained on top of another soakโ.
- The feature looks like an error suppression operator, right?
-
No. Optional Chaining just checks whether some value is undefined or null. It does not catch or suppress errors that are thrown by evaluating the surrounding code. For example:
(function () { "use strict" undeclared_var?.b // ReferenceError: undeclared_var is not defined arguments?.callee // TypeError: 'callee' may not be accessed in strict mode arguments.callee?.() // TypeError: 'callee' may not be accessed in strict mode true?.() // TypeError: true is not a function })()
Specification
See: https://tc39.github.io/proposal-optional-chaining/
Support
Committee Discussions
References
- TC39 Slide Deck: Null Propagation Operator
- es-optional-chaining (@claudepache)
- ecmascript-optionals-proposal (@davidyaha)
Related issues
- Babylon implementation
- estree: Null Propagation Operator
- TypeScript: Suggestion: "safe navigation operator"
- Flow: Syntax for handling nullable variables