Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Closed
RyanCavanaugh opened this issue Jul 15, 2014 · 205 comments · Fixed by #33294
Closed

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

RyanCavanaugh opened this issue Jul 15, 2014 · 205 comments · Fixed by #33294
Assignees
Labels
Committed The team has roadmapped this issue ES Next New featurers for ECMAScript (a.k.a. ESNext) Suggestion An idea for TypeScript Update Docs on Next Release Indicates that this PR affects docs

Comments

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jul 15, 2014

Current Status

  • The TC39 proposal is now at stage 3 (🎉🎉🎉🎉🎉)
  • Implementation is in progress
  • You can expect this feature in TypeScript 3.7
  • We'll update here when it's available in a nightly build
  • Holding off on Optional Call until its semantics are clarified at committee

Open questions

  • What special-casing, if any, should document.all get?

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

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

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

there has been few discussions in esdiscuss about that :

@philipbulley
Copy link
Contributor

👍

@RyanCavanaugh
Copy link
Member Author

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

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

@brian428
Copy link

brian428 commented Oct 1, 2014

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

djarekg commented Oct 1, 2014

I totally agree with brain428

@fdecampredon
Copy link

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

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

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

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

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

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

basarat commented Feb 12, 2015

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

basarat commented Feb 12, 2015

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

@RyanCavanaugh RyanCavanaugh changed the title Suggestion: "safe navigation operator", i.e. x.?y Suggestion: "safe navigation operator", i.e. x?.y Feb 12, 2015
@basarat
Copy link
Contributor

basarat commented Feb 12, 2015

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

rayshan commented Jun 18, 2015

+1

@basarat
Copy link
Contributor

basarat commented Jun 18, 2015

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

@Truebase-com
Copy link

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?

@ExE-Boss
Copy link
Contributor

ExE-Boss commented Jul 27, 2019

Except that document.all can be assigned to a variable, and tracking where it’s used requires a type system, which is why Babel outputs by default:

(_prop = prop) === null || _prop === void 0 ? void 0 : _prop./* do stuff */;

@jhpratt
Copy link

jhpratt commented Jul 27, 2019

I'm aware of that. Babel doesn't have a type system, TypeScript does. Perhaps it's not as simple as I made it sound, but I imagine there's already code for certain situations that is capable of tracking usage.

@G-Rath
Copy link

G-Rath commented Jul 27, 2019

Actually, you don't need a type system to track document.all variables, as the special behaviour is actually on HTMLAllCollection, not document.all.

So you should just be able to do an instanceof HTMLAllCollection check, and you'll be golden.

@noppa
Copy link

noppa commented Jul 27, 2019

Yeah but... why'd you do instanceof when you can just do === null || === void 0? Surely that's more simple.

@G-Rath
Copy link

G-Rath commented Jul 27, 2019

For sure - I was just pointing out you don't need a type system to track document.all :)

Personally I'm tempted to say just break it and see who complains, but it's in the spec, so easiest to just stick with that.

@jhpratt
Copy link

jhpratt commented Jul 27, 2019

@noppa It can be performed at compile time. If foo instanceof HTMLAllCollection is true, emit foo === null || foo === void 0, otherwiise we can safely emit foo == null.

@G-Rath Like it or not, deprecated doesn't mean it shouldn't work. TypeScript should remain compatible with JavaScript.

@ExE-Boss
Copy link
Contributor

@jhpratt But that currently goes against the TypeScript Design Non‑Goals.

Also, you’d still have to do foo === null || foo === void 0 for anything to which HTMLAllCollection could be assigned, eg. any or object, so I don’t think it’s really worth it.

@jhpratt
Copy link

jhpratt commented Jul 27, 2019

I presume you're referring to non-goal (5)

Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.

Though I agree this would certainly emit different code based on the type, it's only to reduce code size. Though as you've pointed out, it's not quite as simple as checking for HTMLAllCollection.

To be fair, TS has rejected a potential minifier that uses type information, and this is (sort of) related — the primary reason to emit == null is to reduce code size based on type information.

If this isn't implemented, it would be great if the lang team adds an option to tsconfig similar to Babel's "loose" option.

After quickly checking, terser automatically converts foo === null || foo === undefined to foo == null, which isn't safe due to this edge case.

@fbartho
Copy link

fbartho commented Jul 28, 2019

If this isn't implemented, it would be great if the lang team adds an option to tsconfig similar to Babel's "loose" option.

Relatedly, many of us use TypeScript for build tools, and for mobile applications, none of which have to worry about browser constraints!

In fact, we use both TS & Babel together, so maybe one of these options should be to passthrough the operator to Babel/underlying runtime!

@Zarel
Copy link

Zarel commented Aug 12, 2019

@fbartho

In fact, we use both TS & Babel together, so maybe one of these options should be to passthrough the operator to Babel/underlying runtime!

I don't understand this comment. You don't need any extra options to passthrough the operator to Babel; if you have TypeScript set up for Babel, you already have noEmit: true which already passes everything through to Babel.

@fbartho
Copy link

fbartho commented Aug 12, 2019

@Zarel Babel’s TypeScript implementation is missing several features that our codebase was already relying on, including namespaces and const enums. We’re using TSC with emit enabled, and applying Babel as a second transformation. (We’re working on getting rid of the namespaces, but it’s unclear if we’ll ever be able to get rid of all of the mismatched features)

@RyanCavanaugh
Copy link
Member Author

People coming to this thread should start at the earlier stage 3 announcement and read the comments starting there (blame GitHub for hiding tons of user content with no straightforward way to load everything)

@domske
Copy link

domske commented Aug 24, 2019

Great feature - "optional chaining" / "safe navigation". Especially in TypeScript strict-mode. Awesome to hear that this will be implemented soon. ❤️

This brought me here and I hope it will be supported. Just an use case:

Expected in TypeScript 3.7.

document.querySelector('html')?.setAttribute('lang', 'en');

VS

Currently in TypeScript 3.5.

const htmlElement = document.querySelector('html');
if (htmlElement) {
  htmlElement.setAttribute('lang', 'en');
}

Will this work without any errors? Or is this still a TypeError: Cannot read property 'setAttribute' of null.? The ? op. should be cancel further chains after null / undefined.

class Test {
  it() {
    console.log('One');
    document.querySelector('html')?.setAttribute('lang', 'en');
    console.log('Two');
  }
}
new Test().it();

I expect following:
If html element does not exist (null). The console should logged One and Two, and the setAttribute method is not tried to invoked. (No errors).
Did I understand that correctly?

@joehillen
Copy link

joehillen commented Aug 24, 2019

@domske FYI, this isn't strictly a TS feature; it's a JS feature.

According to the TC39 proposal the syntax will be:

document.querySelector('html')?.setAttribute?.('lang', 'en');

@microsoft microsoft locked and limited conversation to collaborators Aug 24, 2019
@RyanCavanaugh
Copy link
Member Author

The discussion has started to go circular again so we're back to locked state.

I truly beg anyone tempted to leave a comment in a 100+-comment long GitHub thread to really commit to reading all the prior comments first. Probably your question, and the answer to it, will be found there!

@orta orta added the Update Docs on Next Release Indicates that this PR affects docs label Sep 11, 2019
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Committed The team has roadmapped this issue ES Next New featurers for ECMAScript (a.k.a. ESNext) Suggestion An idea for TypeScript Update Docs on Next Release Indicates that this PR affects docs
Projects
None yet
Development

Successfully merging a pull request may close this issue.