tc39/proposal-optional-chaining

(a?.b).c

samuelgoto opened this issue · 13 comments

This is an open discussion point in the explainer, moving it into an issue to allow us to have a discussion, form an opinion and propagate the resolution back to the explainer.

Should parentheses limit the scope of short-circuting?

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

A really neat property of option I is that it follows along the very simple de-sugaring of a == null ? undefined : a.b.

Can you help me understand what kinds of benefits/use cases one would capture with option II that justifies it breaking the simplicity/consistency of the de-sugaring?

Option 1 makes sense to me, but having added parens change the meaning seems weird.

I still don't see why parenthesis should change the meaning of simple property access:

const a = null;

(a?.b).c // Throws, per Option I

// Why not just write?
a.b.c // Throws

that's also a really good point.

The current state of the spec incidentally* opt for option I, because it is based on syntax only, and in general, parts of a non-separable construct cannot be arbitrary “split” with parentheses. I’m thinking in particular of destructuring assignment:

({x: y}) = b // ReferenceError, because the LHS is interpreted as a plain object literal.

That is distinct from, e.g., (a.b) = c, because a.b and c are two distinct terms that are evaluated separately (technically, that works because the first term evaluates to a so-called Reference).

(*) I say “incidentally”, because (concerning optional chaining) this is an edge case that has zero practical use (whatever option we choose), if you think two seconds about it.

So, seems like there is mostly consensus on Option I? If so, maybe we can (a) close this issue and (b) clarify in the proposal (specifically, maybe remove this section?) that there isn't anything magical about wrapping things with ()s (and point to this thread here in case anyone wants to see the historical discussion on it)?

FWIW C#, Swift, and Coffeescript also have the semantics of Option I:

C# (playground)

class A { public B b; };
class B { public int c; };
A a = null;
int? x;
x = a?.b.c; // null
x = (a?.b).c; // throws

Swift (playground)

class A { var b: B? }
class B { var c: Int = 0 }
let a: A? = nil
var x: Int?
x = a?.b!.c // nil
x = (a?.b)!.c // throws

Coffeescript (playground)

a = null
x = a?.b.c # undefined
x = (a?.b).c # throws

Optional-chained member access should probably have the same operator precedence as other types of member access (19), lower than grouping/parentheses (20): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/Operator_Precedence

So this would correspond exactly to Option 1, which seems to be the consensus anyways. Looking forward to seeing this happen!

We previously discussed this issue in #20 . As a result of that thread, we switched from option II to option I.

@jridgewell Are you convinced by the reasons @claudepache has given here and the discussion in the previous thread? Personally, I find option I more intuitive because you have an easy syntactic way to see the exact scope of short-circuiting.

Option 1 works for me.

What I find difficult about Option 1 is that in (a.b).c, it's obvious that you can remove the parentheses without changing semantics, but you cannot do that in (a?.b).c. That issue came up in estree/estree#146 (comment).

Also, I don't think operator precedence is relevant for this question (unlike @bpartridge suggested). Under both options, parsing is the same, it's just behavior that differs, just as a function body doesn't end at an early return statement, but function execution might.

caub commented

I agree, parentheses shouldn't have any extra special meaning there

Equivalent expressions

(a?.b).c
a?.b.c
(a == null ? undefined : a.b).c

edit: they are not fully equivalent #69 (comment)


Those are equivalent too:

(a?.b ?? {}).c  // (using null coalescing operator, stage 3)
((a == null ? undefined : a.b) ?? {}).c
(a == null ? {} : a.b == null ? {} : a.b).c
a == null ? undefined : a.b == null ? undefined : a.b.c

And those

(a?.b)?.c
a?.b?.c
a == null ? undefined : a.b == null ? undefined : a.b.c

I agree, parenthesis shouldn't have any extra special meaning there

Equivalent expressions:

(a?.b).c
a?.b.c
(a == null ? undefined : a.b).c

Parentheses don’t have extra special meaning, but ?. does. There are several ways to understand the current behaviour, one of them is to imagine that ?. has a lower precedence level than .: compare with a + b * c vs. (a + b) * c.

But this is not something you need to worry about, as you won’t write (a?.b).c except by accident, and we are not able to guess the meaning of accidental code anyway.