tc39 / proposal-optional-chaining

https://tc39.github.io/proposal-optional-chaining/
4.94k stars 75 forks source link

(a?.b).c #33

Closed samuelgoto closed 6 years ago

samuelgoto commented 7 years ago

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?

ljharb commented 7 years ago

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

jridgewell commented 7 years ago

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
ljharb commented 7 years ago

that's also a really good point.

claudepache commented 7 years ago

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.

samuelgoto commented 7 years ago

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

erikdesjardins commented 7 years ago

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
bpartridge commented 6 years ago

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!

littledan commented 6 years ago

We previously discussed this issue in https://github.com/tc39/proposal-optional-chaining/issues/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.

jridgewell commented 6 years ago

Option 1 works for me.

adrianheine commented 5 years ago

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 https://github.com/estree/estree/issues/146#issuecomment-365431078.

adrianheine commented 5 years ago

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 5 years ago

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 https://github.com/tc39/proposal-optional-chaining/issues/69#issuecomment-525304950


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
claudepache commented 5 years ago

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.