• This repository has been archived on 25/Jan/2022
  • Stars
    star
    4,952
  • Rank 8,043 (Top 0.2 %)
  • Language
    HTML
  • Created about 7 years ago
  • Updated over 2 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

Optional Chaining for JavaScript

Status

ECMAScript proposal at stage 4 of the process.

Authors

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):

The following languages have a similar feature. We haven’t checked whether they have significant differences in semantics with this proposal:

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 as foo ? .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] and func?.(arg) look ugly. Why not use obj?[expr] and func?(arg) as does <language X>?

We don’t use the obj?[expr] and func?(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 and func?(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>.

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 to undefined rather than null?

Neither a.b nor a?.b is intended to preserve arbitrary information on the base object a, but only to give information about the property "b" of that object. If a property "b" is absent from a, this is reflected by a.b === undefined and a?.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 number 3 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 for onChange?.() achieve.

Moreover, this ensures that ?. has a consistent meaning in all cases. Instead of making calls a special case where we check typeof foo === 'function', we simply check foo == 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, if a.b is null, then a.b.c will evaluate to undefined, right?

No. It will throw a TypeError when attempting to fetch the property "c" of a.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

  • Engines, see: #115
  • Tools, see: #44

Committee Discussions

References

Related issues

Prior discussion

More Repositories

1

proposals

Tracking ECMAScript Proposals
17,177
star
2

ecma262

Status, process, and documents for ECMA-262
HTML
14,437
star
3

proposal-pipeline-operator

A proposal for adding a useful pipe operator to JavaScript.
HTML
7,380
star
4

proposal-pattern-matching

Pattern matching syntax for ECMAScript
HTML
5,341
star
5

proposal-type-annotations

ECMAScript proposal for type syntax that is erased - Stage 1
JavaScript
4,090
star
6

proposal-temporal

Provides standard objects and functions for working with dates and times.
HTML
3,135
star
7

proposal-observable

Observables for ECMAScript
JavaScript
3,032
star
8

proposal-signals

A proposal to add signals to JavaScript.
TypeScript
2,668
star
9

proposal-decorators

Decorators for ES6 classes
2,640
star
10

proposal-record-tuple

ECMAScript proposal for the Record and Tuple value types. | Stage 2: it will change!
HTML
2,423
star
11

test262

Official ECMAScript Conformance Test Suite
JavaScript
2,073
star
12

proposal-dynamic-import

import() proposal for JavaScript
HTML
1,859
star
13

proposal-bind-operator

This-Binding Syntax for ECMAScript
1,736
star
14

proposal-class-fields

Orthogonally-informed combination of public and private fields proposals
HTML
1,720
star
15

proposal-async-await

Async/await for ECMAScript
HTML
1,577
star
16

proposal-object-rest-spread

Rest/Spread Properties for ECMAScript
HTML
1,496
star
17

proposal-shadowrealm

ECMAScript Proposal, specs, and reference implementation for Realms
HTML
1,365
star
18

proposal-nullish-coalescing

Nullish coalescing proposal x ?? y
HTML
1,233
star
19

proposal-iterator-helpers

Methods for working with iterators in ECMAScript
HTML
1,220
star
20

proposal-top-level-await

top-level `await` proposal for ECMAScript (stage 4)
HTML
1,082
star
21

proposal-partial-application

Proposal to add partial application to ECMAScript
HTML
1,002
star
22

proposal-do-expressions

Proposal for `do` expressions
HTML
990
star
23

agendas

TC39 meeting agendas
JavaScript
952
star
24

proposal-binary-ast

Binary AST proposal for ECMAScript
945
star
25

proposal-built-in-modules

HTML
886
star
26

proposal-async-iteration

Asynchronous iteration for JavaScript
HTML
854
star
27

proposal-explicit-resource-management

ECMAScript Explicit Resource Management
JavaScript
671
star
28

proposal-operator-overloading

JavaScript
610
star
29

proposal-string-dedent

TC39 Proposal to remove common leading indentation from multiline template strings
HTML
588
star
30

proposal-bigint

Arbitrary precision integers in JavaScript
HTML
560
star
31

proposal-set-methods

Proposal for new Set methods in JS
HTML
557
star
32

proposal-import-attributes

Proposal for syntax to import ES modules with assertions
HTML
538
star
33

ecmascript_simd

SIMD numeric type for EcmaScript
JavaScript
536
star
34

proposal-slice-notation

HTML
515
star
35

proposal-change-array-by-copy

Provides additional methods on Array.prototype and TypedArray.prototype to enable changes on the array by returning a new copy of it with the change.
HTML
509
star
36

ecma402

Status, process, and documents for ECMA 402
HTML
506
star
37

notes

TC39 meeting notes
JavaScript
496
star
38

proposal-class-public-fields

Stage 2 proposal for public class fields in ECMAScript
HTML
489
star
39

proposal-iterator.range

A proposal for ECMAScript to add a built-in Iterator.range()
HTML
464
star
40

proposal-uuid

UUID proposal for ECMAScript (Stage 1)
JavaScript
462
star
41

proposal-throw-expressions

Proposal for ECMAScript 'throw' expressions
JavaScript
425
star
42

proposal-module-expressions

HTML
424
star
43

proposal-UnambiguousJavaScriptGrammar

413
star
44

proposal-decimal

Built-in decimal datatype in JavaScript
HTML
408
star
45

proposal-array-grouping

A proposal to make grouping of array items easier
HTML
407
star
46

proposal-async-context

Async Context for JavaScript
HTML
406
star
47

proposal-weakrefs

WeakRefs
HTML
404
star
48

proposal-error-cause

TC39 proposal for accumulating errors
HTML
378
star
49

proposal-ecmascript-sharedmem

Shared memory and atomics for ECMAscript
HTML
376
star
50

proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Shell
376
star
51

proposal-relative-indexing-method

A TC39 proposal to add an .at() method to all the basic indexable classes (Array, String, TypedArray)
HTML
351
star
52

proposal-first-class-protocols

a proposal to bring protocol-based interfaces to ECMAScript users
350
star
53

proposal-global

ECMAScript Proposal, specs, and reference implementation for `global`
HTML
346
star
54

proposal-private-methods

Private methods and getter/setters for ES6 classes
HTML
344
star
55

proposal-numeric-separator

A proposal to add numeric literal separators in JavaScript.
HTML
327
star
56

proposal-private-fields

A Private Fields Proposal for ECMAScript
HTML
320
star
57

proposal-object-from-entries

TC39 proposal for Object.fromEntries
HTML
317
star
58

proposal-module-declarations

JavaScript Module Declarations
HTML
314
star
59

proposal-promise-allSettled

ECMAScript Proposal, specs, and reference implementation for Promise.allSettled
HTML
314
star
60

tc39.github.io

Get involved in specifying JavaScript
HTML
313
star
61

proposal-regex-escaping

Proposal for investigating RegExp escaping for the ECMAScript standard
JavaScript
309
star
62

proposal-await.ops

Introduce await.all / await.race / await.allSettled / await.any to simplify the usage of Promises
HTML
308
star
63

proposal-logical-assignment

A proposal to combine Logical Operators and Assignment Expressions
HTML
302
star
64

proposal-export-default-from

Proposal to add `export v from "mod";` to ECMAScript.
HTML
297
star
65

proposal-promise-finally

ECMAScript Proposal, specs, and reference implementation for Promise.prototype.finally
HTML
278
star
66

proposal-asset-references

Proposal to ECMAScript to add first-class location references relative to a module
268
star
67

proposal-cancellation

Proposal for a Cancellation API for ECMAScript
HTML
262
star
68

proposal-json-modules

Proposal to import JSON files as modules
HTML
259
star
69

proposal-promise-with-resolvers

HTML
255
star
70

proposal-string-replaceall

ECMAScript proposal: String.prototype.replaceAll
HTML
254
star
71

proposal-export-ns-from

Proposal to add `export * as ns from "mod";` to ECMAScript.
HTML
241
star
72

proposal-ses

Draft proposal for SES (Secure EcmaScript)
HTML
217
star
73

proposal-structs

JavaScript Structs: Fixed Layout Objects
216
star
74

proposal-intl-relative-time

`Intl.RelativeTimeFormat` specification [draft]
HTML
215
star
75

proposal-flatMap

proposal for flatten and flatMap on arrays
HTML
215
star
76

proposal-json-parse-with-source

Proposal for extending JSON.parse to expose input source text.
HTML
204
star
77

ecmarkup

An HTML superset/Markdown subset source format for ECMAScript and related specifications
TypeScript
201
star
78

proposal-promise-any

ECMAScript proposal: Promise.any
HTML
198
star
79

proposal-decorators-previous

Decorators for ECMAScript
HTML
184
star
80

proposal-smart-pipelines

Old archived draft proposal for smart pipelines. Go to the new Hack-pipes proposal at js-choi/proposal-hack-pipes.
HTML
181
star
81

proposal-defer-import-eval

A proposal for introducing a way to defer evaluate of a module
HTML
174
star
82

proposal-array-filtering

A proposal to make filtering arrays easier
HTML
171
star
83

proposal-optional-chaining-assignment

`a?.b = c` proposal
168
star
84

proposal-array-from-async

Draft specification for a proposed Array.fromAsync method in JavaScript.
HTML
167
star
85

proposal-extractors

Extractors for ECMAScript
JavaScript
166
star
86

proposal-upsert

ECMAScript Proposal, specs, and reference implementation for Map.prototype.upsert
HTML
165
star
87

proposal-ptc-syntax

Discussion and specification for an explicit syntactic opt-in for Tail Calls.
HTML
165
star
88

how-we-work

Documentation of how TC39 operates and how to participate
161
star
89

proposal-collection-methods

HTML
160
star
90

proposal-Array.prototype.includes

Spec, tests, reference implementation, and docs for ESnext-track Array.prototype.includes
HTML
157
star
91

proposal-error-stacks

ECMAScript Proposal, specs, and reference implementation for Error.prototype.stack / System.getStack
HTML
156
star
92

proposal-promise-try

ECMAScript Proposal, specs, and reference implementation for Promise.try
HTML
154
star
93

proposal-hashbang

#! for JS
HTML
148
star
94

proposal-resizablearraybuffer

Proposal for resizable array buffers
HTML
145
star
95

proposal-import-meta

import.meta proposal for JavaScript
HTML
145
star
96

proposal-intl-segmenter

Unicode text segmentation for ECMAScript
HTML
145
star
97

proposal-extensions

Extensions proposal for ECMAScript
HTML
143
star
98

proposal-seeded-random

Proposal for an options argument to be added to JS's Math.random() function, and some options to start it with.
HTML
143
star
99

proposal-intl-duration-format

141
star
100

proposal-regexp-unicode-property-escapes

Proposal to add Unicode property escapes `\p{…}` and `\P{…}` to regular expressions in ECMAScript.
HTML
134
star