tc39/proposal-optional-chaining

Another approach for syntax

Closed this issue ยท 18 comments

I've been thinking about the syntax of this proposal for quite sometime and still think we should reconsider other options before accepting the current proposed syntax.

@zenparsing has proposed a new syntax (try keyword) and mixed it with the current proposal. This inspired me to re-think about how we can simplify the syntax.

I wanna propose the same idea that I mentioned in this thread. We can introduce the elvis operator (question mark) with curly braces to simple things for parsers.
This proposed syntax works the opposite of this operator in other languages (eg. Kotlin), we apply optional chain in all the parts except it is explicitly declared otherwise in the chain.

By default safely ignoring error for access to property of null of an expression inside the braces

let obj = null;

// property access
const a = ?{obj.a}; // a is undefined

// function call
?{obj.a()}; // no type error

// index and property access
?{obj.a[1]}; // no type error

// deep property access
const e = ?{obj.a.b.c.d.e}; // e is undefined

Declaring explicit exceptions with ~ token (only for properties?)

const obj = { a: null };

// exception for property access
const b = ?{obj.a~.b}; // TypeError: Cannot read property 'b' of null

// exception for function call
?{obj.a~()}; // no type error (maybe invalid syntax?)

// exception for index and property access
?{obj.a~[1]}; // no type error (maybe invalid syntax?)

// exception for private class property
class Check {
  #state = null;
  getBox() {
    return ?{this.#state~.box};
  }
}

// except one deep property access
const d = ?{obj.a.b.c~.d}; // TypeError: Cannot read property 'd' of undefined

[Just an idea]: This syntax might actually support evaluation of an expression inside of curly braces as well:

const obj1 = null;
const obj2 = { a: 'value-a' };

const val1 = ?{ obj1.prop || obj2.a }; // val is 'value-a'
const val2 = ?{ obj1.prop || obj2.b.prop }; // val is undefined

is ?{ } itself an expression? or can it only be used after the assignment (=) operator?

is ?{ } itself an expression? or can it only be used after the assignment (=) operator?

@obedm503 technically speaking in following expression ?{a.b.c} the operator is ?{ } and the operand is a.b.c, although as I mentioned in the last section it could resolve another expression too.

What about deeply nested properties? The following is not really readable:

const obj = null;

const e = ?{?{?{?{obj.a}.b}.c.d}.e};

Reading this operator from left to right is, to me, confusing. Identifying non-null properties, like c, is not easy.

I believe code must be able to be read from left to right without constantly looking back. This proposal however makes this significantly harder.

@MatthiasKunnen sure you can do that even though it seems bizarre to me, but the whole point of this new syntax is to support deeply nested read easier.

Maybe I wasn't clear enough to express it:

const obj = null;

// doesn't matter how deep you access, the chain is safe
const e = ?{obj.a.b.c.d.e}; // undefined

// except you declare otherwise
const d = ?{obj.a.b.c#.d}; // type error cannot read property 'd' of undefined

I see, sorry, I missed that.

Disregarding that I'm not a fan of adding a prefix operator, this proposal, unlike others, will require two new operators. Furthermore, both operators are not seen in other languages both in keyword/character used nor in effect. I could be mistaken about this, are there any? While I'm not opposed to that, it will decrease the support this proposal will receive.

In general, I think it would be best to use existing syntax and a single operator.

Note that # is reserved for private fields, ie obj.#somethingPrivate.

I forgot about that, proposal here (stage 3): Class fields.

Note that # is reserved for private fields, ie obj.#somethingPrivate.

@ljharb sure, but that's different. The private field is leading #, my proposal is trailing.

This would be the optional chain for private class properties:

class MyClass {
  #state = null;
  getTime() {
    return ?{this.#state#.time}; // type error cannot read property 'time' of null
  }
  getBox() {
    return ?{this.#state.box}; // undefined
  }
  ...
}

Worth mentioning the example above would rarely be used as we wanna ignore the exceptions mostly.

@aminpaks having both a.#b and a#.b would be very confusing - i think you'd have a hard time gaining consensus with that conceptual conflict.

@ljharb I think if we forget about expression evaluation within the braces we will have more tokens for exceptional declaration at our disposal.

What do you think of bitwise NOT operator (specially means negative)?

?{obj.a.b~.c};
?{this.#state~.prop};

Personally I still think the current proposal is the best form; having to create a special "block" in which navigation operates differently strikes me as another language mode, which adds lots of confusion to a reader.

I understand. I tried to make sense and reason to propose the best readable form.
IMHO the current proposal is great for access property but not for the other two.

In comparison looking at propA?.propB?.propC?.() vs ?{ propA.propB.propC() } I would say the second one is more readable.

having to create a special "block" in which navigation operates differently strikes me as another language mode

@ljharb I thought about what you said and still not convinced. If what you say is true then you should feel the same for try/catch.

Navigation operates the same in try/catch; itโ€™s the exception behavior thatโ€™s different.

I've been thinking about it a LOT and I agree with op about ?{} syntax as it is more convenient, it resembles to try{}catch{} blocks, having another block seems like a better alternative to having some syntax sugar like proposed ?.

Also, having ?{} will allow the community to grow in a more syntax block oriented way for future proposals without having to deal with not readable syntax.

Example:

let a = {b: {c: { d: 5, print: (...msg)=>console.log(...msg)} } };
?{a.b.c.d} // 5
?{a.b.c.d.e} // undefined

?{a.b.c.print('hi')} // prints hi
?{a.b.c.nonexistant('hi')} // undefined
// compared to
a?.b?.c // 3
a?.b?.c?.d // undefined

a?.b?.c?.print?.('hi') // prints hi
a?.b?.c?.nonexistant?.('hi') // undefined

We must think about it better so we dont regret this decision in the next 10 years.

@ChoqueCastroLD there are many reasons that would never be tenable, not the least of which that it doesn't let you make some of the properties optional and some required.

@ljharb What about having ?? instead of ?.

Seems more readable for () and [],

obj?.prop       // optional static property access
obj?.[expr]     // optional dynamic property access
func?.(...args) // optional function or method call
// will be
obj??prop       // we might use the dot aswell obj??.prop
obj??[expr]     // optional dynamic property access
func??(...args) // optional function or method call

?? Is already in use for nullish coalesing.

But even if it wasn't this propasal is stage 3 and it's already implemented in several systems. Its syntax won't change. There is a reason all these syntax issues are closed. Discussing this further is a waste of everyone's time. Especially since every suggestion seems to have been made before.