tc39/proposal-optional-chaining

Is this feature over-constrained?

zenparsing opened this issue · 34 comments

First, thanks for all of the great work that has been done on this proposal!

However, I'm concerned that this proposal is over-constrained:

  1. There is a clear pain point here that we want to address.
  2. For a variety of reasons, we want to reuse syntax from other languages.
  3. We want JavaScript's syntax to be self-similar.
  4. We would like to have the full complement of "optional ops": property lookup, computed property lookup, and function calls.
  5. The ternary operator prevents us from using the familiar syntax for computed properties and function calls.

Taken together, these constraints appear unresolvable.

  • Doing nothing violates contraint 1.
  • ?.[ and ?.( violate contraints 2 and 3.
  • Eliminating ?.[ and ?.( violates contraint 4.
  • ?[ and ?( violate contraint 5.
  • Other syntax options violate constraint 2.

The current proposal (with ?.[ and ?.() sacrifices contraint 2 and 3. I am most worried about sarificing self-similarity (3). Syntactic features pay for themselves by making the surface of the language simpler in common usage, and I worry about the extent to which novel combinations of characters increases overall complexity.

So far I don't think that we have considered eliminating constraint 5 (the ternary operator problem). In the interest of thinking outside of the box, I'm going to throw an idea out (that might be good or bad).

Since ternary is the problem, what if there were a syntactic context in which optional chaining is allowed but ternary is not? A prefix unary operator might do the trick. I'm going to use try, but maybe there's another option.

Rewriting some examples from the readme:

let fooValue = try myForm.querySelector('input[name=foo]')?.value;

try iterator.return?() // manually close an iterator

if (try myForm.checkValidity?() === false) { // skip the test in older web browsers
    // form validation fails
    return;
}

Since ternary is the problem, what if there were a syntactic context in which optional chaining is allowed but ternary is not?

I came to the same conclusion in #52.
I like where you are going with this.

what if there were a syntactic context in which optional chaining is allowed but ternary is not?

It is unacceptable for me to be unable to use ternary operator and optional chaining in the same expression (e.g., a?[b ? c : d]). A better solution would be that, in such a syntactical context, the token ? of the ternary operator cannot be directly followed by one of the tokens [ or ( unless there is an intervening space. (I think it is how Swift handles it, without separate context of course.) For instance:

// inside the syntactic(al) context:
a?[b] // optional chaining
a ? [b] : c // conditional operator
a ?[b] : c // optional chaining followed by syntax error

(That said, I am skeptical that introducing such a syntactic context will be acceptable. It looks like a "use new syntax" directive in disguise.)

A better solution would be that, in such a syntactical context, the token ? of the ternary operator cannot be directly followed by one of the tokens [ or ( unless there is an intervening space.

As much as that would solve and make this entire proposal so much simpler, it fails the primary directive of backwards compatibility. If that weren’t the case, I’m sure this proposal would have been stage 4 by now

@dustinsavery I think you missed the ”syntactic context” proposed by @zenparsing.

@claudepache I saw it. I think it makes sense. I was just commenting on the simpler (and mentally, more straight forward) option that you were commenting on. Unless you're suggesting yours in conjunction with his.

try a?[b]

Thinking of the grammar, try a?[b ? c : d] could be directly supported. Inside the [] is new grammar production, you'd need another try inside it to use a chain:

try a?[try b?.c]

@dustinsavery So, maybe you missed the “syntactical context” words in my comment that were intended to contextualise it (the comment, not the context) to the “syntactic context” of @zenparsing? OK, I’m gonna edit my comment and repeat my contextualiser at the beginning of the code snippet (and decontextualise the parenthesis about Swift, btw).

@claudepache I see where I misunderstood. That makes more sense, I think we're on the same page now.

I really like this proposal! I guess my only concern is, the try keyword seems to suggest general error suppression (which this isn't), rather than null/undefined checking

@littledan what about a different keyword if the syntax is not a problem?

const obj = { a: { b: false } };

opt obj?.a?.b
console.log(opt obj?.a?.b); // false
console.log(opt obj?.b?.c); // undefined

emmm, maybe we should think of a new syntax completely?
This could be similar to do expression but with safety in regard with null pointer exceptions.

const obj = { a: { b: false } };

safe{obj.a.b}; // false
safe{obj.a.b.c.d.e.f}; // undefined

I find the keyword{} proposal interesting but too lengthy/verbose IMHO.

@Mouvedia I understand.
There are a couple of things to consider:

  • The syntax of this proposal is already noisy for array index and function obj?.myFunction?.()
  • The new syntax offers a new way to escape the ternary problem
  • In terms of keyword we should discuss

@zenparsing can we drop the ? if we are checking every properties?

try a[foo].c() would expand to try a?[foo]?.c?()

@Mouvedia then how would you express in a.b.c.d that you wanted the .b and the .d to be optional, but the .c to throw if a.b was non-nullish?

@ljharb Why can't we look at this problem in the opposite way?

const obj = { a: { b: { c: false } };

safe{a.b}; // false
safe{a.e.a}; // undefined
safe{a.e!.a}; // throw  TypeError: Cannot read property 'a' of undefined

That's certainly a reasonable alternative suggestion!

Separate from bikeshedding (safe is an existing valid identifier; and ! has many connotations, including negation, mutation, "does not throw an error", "throws an error", etc - I think it's been discussed previously in this repo), my personal intuition is that the common case might not be "every member access needs to be optional", and also that making that the default implicit behavior could hide bugs (when a link in the chain was not expected to be "possibly nullish").

@ljharb

We can discuss about the syntax (keywords, token).
The point here is if we use a specific syntax for optional chain return why not the default behaviour be the positive?

I understand that your argument that it might lead to bugs because devs may forget. But my counter argument is that's why safe{...} is verbose and emphasizes the fact that chains are safely returned.

Zarel commented

While this is a really interesting and cool suggestion, I vastly prefer the proposal in its current state.

if a.b was non-nullish

Your meant nullish there right?
@ljharb simply try a?.b.c?.d.

@Mouvedia ah so you were only suggesting the implicit optional when the keyword was used with no ? - gotcha. Makes sense, but that seems confusing to me to have one magic variant.

but that seems confusing to me to have one magic variant.

Yeah that's why I was asking. I find it useful but that might be too much.

I can chime in with some experience using this in production.

The vast majority of the time, we use it for access. I don't think I have used it with bracket notation, but I have used it like this:

if (input?.trim?.())
  processString(input.trim())

It is not a common use case, but it does occur. I like the syntax. It is very bugfix friendly!

@adrianhelvik Hmm, I don't quite understand that example. Why are you calling the trim method twice? If you want to check for its existence, would it make sense to write it like this?

if (input?.trim)
  processString(input.trim());

Why are you calling the trim method twice?

It's to cover an edge case: the string only contains whitespace. If you know that's a string the second ?. is over the top though.

Oh I see, thanks for explaining.

I used it in a case where the input wasn't always a string as data from the previous component wasn't removed before mounting another component (receiving the same props).

Not a good long term solution, but it let me quickly fix a bug through the online editor at BitBucket.

I've been recently trying to pitch in the optional chaining proposal. After having spoken to a couple of members, the general feeling is that while the prefix try keyword is nice in that it makes things a little more explicit, it will feel heavy if you have to apply it to every nullish situation - ?. is common enough that it will be annoying to go back and add the try. And if try is only required for ?( and ?[, then that's just a different inconsistency, which is at least on par with requiring a ?. prefix to ( and [.

I think all of these designs have some sort of tradeoff, and the current direction of using the ?., ?.[ and ?.( operators are each relatively isolated in their behavior, and will satisfy most of the common use-cases.

This is a bit of a tangent, but it will impact syntax: in which actual use case would it be a problem to deeply evaluate a chain?

The logic being that if there isn't, then the syntax can be simplified to only using one operator in the chain, which could make things easier.

@rijnhard When I read let x = foo?.bar.baz, I think that okay, foo may or may not be initialized, but if it is, it has a bar property that has a baz property.

let x = try foo.bar.baz doesn't teach me anything about the data structure. If someone wrote let x = foo?.bar?.baz I would be prepared for a sloppy data structure and and act accordingly.

I'm pretty unopiniated with regards to try as a separate feature, but I am very happy with ?..

Very good point @adrianhelvik. I also think ?. is a good choice, especially due to its similarity with other languages’ implementations of the same feature.

Personally I believe this feature has been discussed more than enough and that it’s time to get some momentum on it :)

Personally I believe this feature has been discussed more than enough and that it’s time to get some momentum on it :)

I wish we had it already. Working on Alexa Skills in Node.js there are a lot of deeply nested properties that often need to be checked. For example, to see if geolocation is supported on a device making a request, I have to do:

function supportsGeolocation({ requestEnvelope: { context } }) {
  return context &&
    context.System &&
    context.System.device &&
    context.System.device.supportedInterfaces &&
    context.System.device.supportedInterfaces.Geolocation;
}

If I had Optional Chaining then I could convert that function into:

function supportsGeolocation({ requestEnvelope: { context } }) {
  return context?.System?.device?.supportedInterfaces?.Geolocation;
}

Unfortunately, even if Optional Chaining were approved tomorrow, it would be at least into the middle of 2020 at minimum before Amazon Web Services would support a Node version with the feature. More than likely 2021.

fabb commented

Unfortunately, even if Optional Chaining were approved tomorrow, it would be at least into the middle of 2020 at minimum before Amazon Web Services would support a Node version with the feature. More than likely 2021.

Babel?

Babel?

Yes, that would certainly be an option. It would be no problem to have Babel make the code compatible with Node 10.x. There is also the option to create a custom runtime for AWS that runs the latest Node version but for me the cost/benefit ratio (time investment to deploy it) is not worth it. Babel would be the way to go.

Closed by b7529f2.