tc39 / proposal-optional-chaining

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

Another approach for syntax #78

Closed aminpaks closed 5 years ago

aminpaks commented 5 years ago

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

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

aminpaks commented 5 years ago

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.

MatthiasKunnen commented 5 years ago

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.

aminpaks commented 5 years ago

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

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.

ljharb commented 5 years ago

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

MatthiasKunnen commented 5 years ago

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

aminpaks commented 5 years ago

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.

ljharb commented 5 years ago

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

aminpaks commented 5 years ago

@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};
ljharb commented 5 years ago

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.

aminpaks commented 5 years ago

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.

aminpaks commented 5 years ago

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.

ljharb commented 5 years ago

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

ChoqueCastroLD commented 4 years ago

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.

ljharb commented 4 years ago

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

ChoqueCastroLD commented 4 years ago

@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
MatthiasKunnen commented 4 years ago

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