microsoft / TypeScript

TypeScript is a superset of JavaScript that compiles to clean JavaScript output.
https://www.typescriptlang.org
Apache License 2.0
99.4k stars 12.32k forks source link

Suggestion: "safe navigation operator", i.e. x?.y #16

Closed RyanCavanaugh closed 4 years ago

RyanCavanaugh commented 10 years ago

Current Status

Open questions


C# and other languages have syntax sugar for accessing property chains where null (or in our case, undefined) might be encountered at any point in the object hierarchy.

var x = { y: { z: null, q: undefined } };
console.log(x?.y?.z?.foo); // Should print 'null'
console.log(x?.baz); // Still an error
console.log(x.y.q?.bar); // Should print 'undefined'

Need proposal on what exactly we should codegen, keeping in mind side effects of accessors.


Edit by @DanielRosenwasser February 27, 2018: This proposal is also called the "null propagation" operator.

JsonFreeman commented 10 years ago

So in the first example, we might emit it like the following:

x && x.y && x.y.z && x.y.z.foo

But then we'd have to somehow make x, y, z, and foo each evaluate at most once.

DanielRosenwasser commented 10 years ago

You also really can't do && in many cases because truthiness becomes a bit of a problem for primitives.

For example:

"     "?.trim()?.indexOf("hello")

gives "".

So you need to do some explicit comparisons to null using == for the general case, unless we leverage the type system (which would be fairly cool to see us do).

We could possibly emit a monadic-bind function (not pretty for the JS output), or use some transformation on ternary operators (closer to typical JS equivalent). I'm clearly a little biased towards the latter.

fdecampredon commented 10 years ago

there has been few discussions in esdiscuss about that :

philipbulley commented 9 years ago

:+1:

RyanCavanaugh commented 9 years ago

Ideally we'd have ES7 (or ES8 or ES9 or ...) implement this first since there'd probably be some disagreement about the exact semantics about whether or not to actually use 0/"" as falsy primitives for the purposes of any operator here.

NoelAbrahams commented 9 years ago

:+1: I'd like to see TypeScript gets this in first without having to wait for ESxx.

brian428 commented 9 years ago

The fact that simple and insanely useful null-safety operators like "?." and "?:" AREN'T in the ES6 spec means the people putting together the ES6 spec should be hanging their heads in shame. This is such a simple and obvious thing that to not incorporate it would frankly be insane. There's a reason most modern languages support these: they're indispensable.

I realize this would be a deviation from the current spec (since the current spec is so short-sighted as to omit this). But it's so ridiculously useful that I think this single deviation would be justified. The vast (VAST) majority of TS developers wouldn't be affected by minor changes to the implementation, if or when this finally gets added to an ES specification. The huge benefits this would offer is worth the potential future impact to a tiny fraction of developers. And given the laughably slow ES spec process, this wouldn't even matter at all for several years (at minimum).

djarekg commented 9 years ago

I totally agree with brain428

fdecampredon commented 9 years ago

@brian428 the problem here is that that operator maybe implemented in ES7 so if typescript go with a specification that ends up differing from the final ES7 one, nobody will be happy.

NoelAbrahams commented 9 years ago

the problem here is that that operator maybe implemented in ES7 so if typescript go with a specification that ends up differing from the final ES7 one, nobody will be happy.

I think it is a more positive approach for TypeScript to implement features that may _potentially_ (or may not) make it into a future ES version, because it will be a useful testbed for influencing ES direction.

Here is an example of ES discussion being influenced by TypeScript:

The TypeScript... option to declare and initialize via a private prefix on one of constructor's parameters would be helpful to many developers

Furthermore, it's certainly possible for ES to adopt a feature that is already present in TypeScript, but with different semantics (for example, around how modules work).

RyanCavanaugh commented 9 years ago

it's certainly possible for ES to adopt a feature that is already present in TypeScript, but with different semantics

I should note that we broadly consider this to be a worst-case scenario. We really wanted modules in ES6 to be finalized before we declared TypeScript 1.0, but the committee's schedule delays prevented that. This is something to be avoided, not repeated. We'd really like to hit features that have either a ~0% chance of making it into ES7+ (e.g. type annotations), or have a ~100% chance of making it in with easily-defined semantics (e.g. where fat arrow was two years ago). New operators are likely to fall in the awkward middle.

philipbulley commented 9 years ago

In the worst case, if ES7 does differ, could a compiler flag support the legacy TS implementation, thus offering a grace period? This coupled with clear migration documentation should offer developers a straightforward route to any new standard.

Ultimately, use of any such featureโ€”although insanely usefulโ€”isn't essential by developers. TS should make potential future implications of it's usage abundantly clear from day one. Don't like the idea of a potential managed refactor, don't use it. Perhaps an opt-in compiler flag to enforce this message?

TS shouldn't go wild with wanting to influence ES, but in small isolated cases such as this, it'd be a shame if TS were to completely shy away.

kevinbarabash commented 9 years ago

Maybe we could put together a strawman proposal for this and then have a reference implementation behind a --harmony flag (or something like that). That way we can drive ES7 development of this feature.

metaweta commented 9 years ago

To prevent side-effects due to repeated look-ups, the compiler will either have to output temporary variables:

($tmp0 = x, $tmp0 === void 0 ? void 0 : 
    ($tmp1=$tmp0.y,  $tmp1 === void 0 ? void 0 : 
        ($tmp2 = $tmp1.z,  $tmp2 === void 0 ? void 0 : $tmp2)))

or use a memoizing membrane based on Proxy.

From a categorical point of view, this is just the maybe monad applied to property lookup, so it's a very natural feature for a language where all property lookups may return undefined. I'd be surprised if ES7 adopted any semantics other than the one described by the code above.

basarat commented 9 years ago

The codeplex issue had quite a number of votes (61)

I really badly need this to ease the pain of using atom for atom-typescript.

It is very idiomatic in coffescript code (although I would like it not to be as popular as determinism is better than a fudgy ?). Open any coffescript file, especially one that works with the DOM directly like space-pen (where functions can run after the view is destroyed or before the view is attached) and you will find a gazillion ? usages. e.g. this file has 16 https://github.com/atom-community/autocomplete-plus/blob/f17659ad4fecbd69855dfaf00c11856572ad26e7/lib/suggestion-list-element.coffee

Again I don't like that I need this, but its the state of JavaScript, and I'd rather ? than a million if( && fest ) { then }

But I really really need it to keep my code readable. Its also very common to need this when you are waiting for an XHR to complete and angular runs its digest loop.

basarat commented 9 years ago

Okay now I have read the thread and see why we are waiting. I understand sigh.

basarat commented 9 years ago

we'd have to somehow make x, y, z, and foo each evaluate at most once.

coffeescript does do some optimizations e.g. stores intermediate access results:

typeof foo !== "undefined" && foo !== null ? (ref = foo.bar) != null ? ref.baz() : void 0 : void 0;

(I strongly feel the undefined check is unnecessary for typescript : as we should have a var init typechecked by typescript)

rayshan commented 9 years ago

+1

basarat commented 9 years ago

In news today, Dart is getting official support for it : https://github.com/gbracha/nullAwareOperators/blob/master/proposal.md

Truebase-com commented 9 years ago

Very important feature.

Possibly an off-the-wall idea but the codegen for this feature could be done very easily without side effects if everyone decided that it would be OK to handle the feature with keyed property access:

if (aaa?.bbb?.ccc) {}

Could compile to

if (__chain(aaa, "bbb", "ccc")) {}

A __chain function would have to be emitted similar to __extends. The __chain function could just iterate through the arguments array, returning null when the upcoming member is not instanceof Object or does not contain the member name. Function calls could be handled by passing in an array as a parameter (and using .apply() under the covers), so ...

if (aaa?.bbb?.ccc?(1, 2, 3)) {}

Could compile to

if (__chain(aaa, "bbb", "ccc", [1, 2, 3])) {}

This would also keep the generated JS reasonably idiomatic, even for long chains.

Needs refinement obviously ... but maybe there is something here?

kevinbarabash commented 9 years ago

If aaa?.bbb?.ccc? returns the value of a.b.c if all props exist and it happens to be a function, then couldn't

if (aaa?.bbb?.ccc?(1, 2, 3)) {}

compile to

if (__chain(aaa, "bbb", "ccc")(1, 2, 3)) {}

?

Truebase-com commented 9 years ago

@metaweta: Your solution is only checking for void 0 ... doesn't that kind of defeat the purpose of the feature?

What about this:

var result = one?.two?.three;

Generates:

var $a, $b, $c;
var result = $a = one, $b = $a ? $a.two : void 0, $b ? $b.three : void 0;

I'm pretty sure this handles all the cases. Function calls would probably need an instanceof Function check.

(Minor downside here of unexpected local variables being emitted ... could be wrapped in an IIFE maybe)

Truebase-com commented 9 years ago

@kevinb7: What happens if ccc isn't a function? With the code you've described, __chain would always have to return a valid function, or a TypeError would be emitted.

metaweta commented 9 years ago

@Back-io When a property is absent, a lookup returns undefined === void 0. Your solution fails for looking up properties of falsey values like empty string and zero.

Truebase-com commented 9 years ago

@metaweta: No it doesn't: http://jsfiddle.net/25LppbL6/

Truebase-com commented 9 years ago

Also, I would imagine the TS team isn't crazy about using loose equality due to the fact that some people's linters warn against it.

metaweta commented 9 years ago

@Back-io Yes it does: http://jsfiddle.net/25LppbL6/2/

metaweta commented 9 years ago

@Back-io Regarding null, I'm not sure what the intended semantics of a?.b is. If it's "If the property b is defined then use it", then my code is almost correct. The only way you'd get null is if it's assigned to be null, because lookups of nonexistent properties return undefined. It does fail to catch the case where the property exists but is set to undefined. To be completely correct, it would check with Object.hasOwnProperty() instead of comparing to void 0 === undefined.

If the semantics are "if the property b is truthy then use it", your code is fine and to some extent matches JS idiom.

Truebase-com commented 9 years ago

Umm ... unless I'm missing something ... the changes you made to the fiddle just further prove me right ... var result is still undefined in all 3 cases. And I'm not sure what case you're trying to bring forward by extending the primitive prototypes ...

Truebase-com commented 9 years ago

I'm fairly certain that the behavior of the feature would be to short circuit with void 0 instead of generating an error. (I like your idea above that it should always return void 0 in the case of a missing member).

metaweta commented 9 years ago

What is the intended output of ''?.toString?

kevinbarabash commented 9 years ago

@kevinb7: What happens if ccc isn't a function? With the code you've described, __chain would always have to return a valid function, or a TypeError would be emitted.

Good point.

Truebase-com commented 9 years ago

@metaweta: I would imagine most people would expect a reference to the toString function, so I see where your point, It does break down somewhat in the case where you're accessing the prototype members of 0, false, or ''.

PatrickJS commented 8 years ago

:+1: Angular 2 added Elvis Operator to their template syntax

basarat commented 8 years ago

@metaweta :

What is the intended output of ''?.toString?

If you meant ''?.toString() it would be :

if ('' != null) {
  ''.toString();
}

Sample

basarat commented 8 years ago

Maybe we could put together a strawman proposal for this

@kevinb7 it already exists : http://wiki.ecmascript.org/doku.php?id=strawman:existential_operator

basarat commented 8 years ago

I did some quick tests to implement this on top of PropertyAccessExpression as its special case but didn't work out well as we really need ?. to be right associative (instead of left associative like .) otherwise the emitter becomes needlessly complex.

There is comment by BrendenEich here that reflects this as well.

zlumer commented 8 years ago

I did some quick tests to implement this on top of PropertyAccessExpression as its special case but didn't work out well as we really need ?. to be right associative (instead of left associative like .) otherwise the emitter becomes needlessly complex.

@basarat can you please elaborate on that? An example of the difference between right associative and left associative ?. would be very helpful to me.

basarat commented 8 years ago

@zlumer

An example of the difference between right associative and left associative ?. would be very helpful to me.

. is left associative so the the expression foo?.bar?.baz becomes in AST (if we treat ?. the same):

                    // foo.bar.baz = PropertyAccessExpression
                    //   .expr foo.bar =  PropertyAccessExpression
                    //     .expr foo = Identifier
                    //     .name bar = Identifier
                    //   .name baz = Identifier

The JavaScript emit needed is

foo != null ? (ref_1 = foo.bar) != null ? ref_1.baz() : void 0 : void 0;

Its just easier to do this emit (especially recursively) if we had the following in the AST:

                    // foo.bar.baz = PropertySafeAccessExpression
                    //   .name foo =  Identifier
                    //   .expr bar.baz = PropertySafeAccessExpression
                    //      .expr bar = Identifier
                    //      .name baz = Identifier

Just think of how would you convert the first AST to JavaScript and the complexities would be clearer. Hope this helps :rose:

DanielRosenwasser commented 8 years ago

@zlumer, to add to what @basarat said, I'll just put out the example 1 + 2 + 3.

When parsing, we have to decide which of these operations will happen first.

If + is left-associative, this will be interpreted as ((1 + 2) + 3).

If + is right-associative, this will be interpreted as (1 + (2 + 3)).

You might question whether this would actually make a difference. In JavaScript it would! Consider the example "hello" + 2 + 3.

(For the record, JavaScript/TypeScript use left-associativity for the + operator.)

DanielRosenwasser commented 8 years ago

@basarat, my understanding of what @BrendanEich said (and he can correct me if I'm wrong - sorry for the ping!) is not that ?. is right-associative, but that CoffeeScript special cases property accesses on the right of the ?. to be right-associative. For example, it will parse

o.p?.q.r.s

as

((o . p) ?. (q . (r . s))) # or something close to this

instead of

((((o . p) ?. q) . r) . s)

because it's easier to emit.

Our AST needs to lend well to sane semantic analysis, so our emitting procedure can afford to be slightly more complex to suit this need.

zlumer commented 8 years ago

@basarat @DanielRosenwasser thank you for the explanations. So far I understand it, but I'm still not certain about one thing. The left-associative ?. is pretty obvious and expected:

foo?.bar?.baz

Becomes (approx.):

var ref = ((ref = foo) == null) ? null : ((ref = ref.bar) == null) ? null : ref.baz;

But I don't understand at all how would a right-associative ?. work. Can you please provide an example?

basarat commented 8 years ago

But I don't understand at all how would a right-associative ?. work. Can you please provide an example

@zlumer The runtime behaviour will be left associative. I was just talking about the AST as DanielRosenwasser also clarified: is not that ?. is right-associative, but that CoffeeScript special cases property accesses on the right of the ?. to be right-associative.

Our AST needs to lend well to sane semantic analysis, so our emitting procedure can afford to be slightly more complex to suit this need.

@DanielRosenwasser thanks for the feedback :rose:

zlumer commented 8 years ago

@basarat thank you, suddenly it all became clear :smiley:

schungx commented 8 years ago

Similar to @basarat's Feb comment, I.... sigh...

However, if you think about it, 99% of the use cases of this will be to check against a null pointer to an object. Frankly speaking, who does x?.b?.c when x is a number? There just aren't many real-life use cases for long chains when we are not talking about an object (with the possible exception of string). For short chains, I think we can live with x && x.b or x === 0 ? null : x.b.

So, can we, sorta, say ?. only works on object types? Any other type throws a syntax error. And disallow function calls in side the chain.

Then the whole thing transcribes to a && a.b && a.b.c.

Truebase-com commented 8 years ago

@schungx What if a member in the chain is an "any" type? Disallow completely? Or just let it through and hope for the best?

schungx commented 8 years ago

Well, my suggestion? Disallow completely. Inelegant as hell, I know... :-)

But my rationale:

  1. This is a short-hand, so if somebody is using any, just use long-hand.
  2. If somebody is using TypeScript, most likely he/she is using it for the typing support, so hopefully he/she won't have many any's around!
  3. any really should be handled with care. Allowing usage of such short-hands with a flexible type like any is really asking for bugs to happen. In my opinion, any should be as limited as possible. It is sort of like C's (void *) -- the fact that you're handed a nuke doesn't mean you must trigger it just because you can!
dotnetwise commented 8 years ago

This would be amazing operator!! Especially for ES6/ES7/TypeScript

var error = a.b.c.d; //this would fail with error if a, b or c are null or undefined.
var current = a && a.b && a.b.c && a.b.c.d; // the current messy way to handle this
var currentBrackets = a && a['b'] && a['b']['c'] && a['b']['c']['d']; //the current messy way to handle this
var typeScript = a?.b?.c?.d; // The typescript way of handling the above mess with no errors
var typeScriptBrackets = a?['b']?['c']?['d']; //The typescript of handling the above mess with no errors

However I propose a more clear one - as not to confuse ? from the a ? b : c statements with a?.b statements:

var doubleDots = a..b..c..d; //this would be ideal to understand that you assume that if any of a, b, c is null or undefined the result will be null or undefined.
var doubleDotsWithBrackets = a..['b']..['c']..['d'];

For the bracket notation, I recommend two dots instead of a single one as it's consistent with the others when non brackets are used. Hence only the property name is static or dynamic via brackets.

Two dots, means if its null or undefined stop processing further and assume the result of expression is null or undefined. (as d would be null or undefined).

Two dots make it more clear, more visible and more space-wise so you understand what's going on.

This is not messing with numbers too - as is not the same case e.g.

1..toString(); // works returning '1'
var x = {};
x.1 = {y: 'test' }; //fails currently
x[1] = {y: 'test' }; //works currently 
var current = x[1].y; //works
var missing= x[2].y; //throws exception
var assume= x && x[2] && x[2].y; // works but very messy

About numbers two options: Your call which one can be adopted, but I recommend first one for compatibility with existing rules!

  1. Should fail as it does now (x.1.y == runtime error)
var err = x..1..y; // should fail as well, since 1 is not a good property name, nor a number to call a method, since it's after x object.
  1. Should work since it understands that is not a number calling a property from Number.prototype
var err = x..1..y; // should work as well, resulting 'test' in this case
var err = x..2..y; // should work as well, resulting undefined in this case

With dynamic names:

var correct1 = x..[1]..y; //would work returning 'test'
var correct2 = x..[2]..y; //would work returning undefined;

What do you think folks?

P.S. foo?.bar and foo?['bar'] syntax would work too.

However the using both current ? : operator and ?. might be very confusing on the same line.

e.g. using ?. and ?['prop']

var a = { x: { y: 1 } };
var b = condition ? a?.x.?y : a?.y?.z;
var c = condition ? a?['x']?['y'] : a?['y']?['z'];

as opposed to double dots .. and ..['prop']

var a = { x: { y: 1 } };
var b = condition ? a..x..y : a..y..z;
var c = condition ? a..['x']..['y'] : a..['y']..['z'];
Which one does look more clear to you?
hsablonniere commented 8 years ago

Very interesting. :+1:

schungx commented 8 years ago

IMHO, two dots will be more confusing. There are languages where two dots represent a range (e.g. 1..4) and TypeScript may add this feature in the future.

A question mark also has the semantic meaning of uncertainty or conditional and two dots do not convey the same meaning.