Skip to content
This repository has been archived by the owner on Jan 25, 2022. It is now read-only.

(a?.b).c #33

Closed
samuelgoto opened this issue Sep 22, 2017 · 13 comments
Closed

(a?.b).c #33

samuelgoto opened this issue Sep 22, 2017 · 13 comments

Comments

@samuelgoto
Copy link

samuelgoto commented Sep 22, 2017

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
Copy link
Member

ljharb commented Sep 22, 2017

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

@jridgewell
Copy link
Member

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
Copy link
Member

ljharb commented Sep 22, 2017

that's also a really good point.

@claudepache
Copy link
Collaborator

claudepache commented Sep 22, 2017

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
Copy link
Author

samuelgoto commented Sep 22, 2017

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
Copy link

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
Copy link

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
Copy link
Member

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.

@jridgewell
Copy link
Member

Option 1 works for me.

@adrianheine
Copy link

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

@adrianheine
Copy link

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
Copy link

caub commented Aug 27, 2019

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

@claudepache
Copy link
Collaborator

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.

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants