Skip to content

ralmn/proposal-optional-chaining

 
 

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

29 Commits
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Optional Chaining for JavaScript

Status

Current Stage:

  • Stage 1

Authors

  • Claude Pache (@claudepache)
  • Gabriel Isenberg (@the_gisenberg)

Overview and motivation

When looking for a property value deeply in a tree structure, one has often 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

Prior Art

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

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 to the current property access, method or function call, but also the whole chain of property accesses, method and function calls following directly 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 [TODO: provide precise references].

Stacking

Let’s call Optional Chain an Optional Chaining operator followed by a chain of property accesses, method and 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

Should parentheses limit the scope of short-circuting?

(a?.b).c
(a == null ? undefined : a.b).c  // this?
a == null ? undefined : (a.b).c  // or that?

Given that parentheses are useless in that position, it should not have impact in day-to-day use.

Unless there is a strong reason for one way or the other, the answer will mostly depend on how the feature is specified.

Optional deletion

Because the delete operator is very liberal in what it accepts, we have that feature for free:

delete a?.b
// delete (a == null ? undefined : a.b) // that *would* work if `? :` could return a Reference...
a == null ? undefined : delete a.b      // this is what we get, really

Optional assignment

Should optional property assignement as in: a?.b = c be implemented? This is supported and used in CoffeeScript.

Not supported

The following are not implemented for lack of real-world use cases.

  • optional construction: new a?.()
  • optional template literal: a?.`{b}`
  • constructor or template literals in/after an Optional Chain: new a?.b(), a?.b`{c}` .

All the above cases will be forbidden by the grammar.

FAQ

[TODO: to be completed. In particular, discuss specific criticisms around long short-circuiting.]

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 distinguish efficiently 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 have each 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 occurs 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>.

Why does (null)?.b evaluates 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 property; therefore, (null)?.b is undefined.

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.

Specification

See: https://tc39.github.io/proposal-optional-chaining/

TODO

Per the TC39 process document, here is a high level list of work that needs to happen across the various proposal stages.

  • Identify champion to advance addition (stage-1)
  • Prose outlining the problem or need and general shape of the solution (stage-1)
  • Illustrative examples of usage (stage-1)
  • High-level API (stage-1)
  • Initial spec text (stage-2)
  • Babel plugin (stage-2)
  • Finalize and reviewer signoff for spec text (stage-3)
  • Test262 acceptance tests (stage-4)
  • tc39/ecma262 pull request with integrated spec text (stage-4)
  • Reviewer signoff (stage-4)

References

Related issues

Prior discussion

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages

  • HTML 100.0%