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鈥檒l occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
RyanCavanaugh opened this issue Jul 15, 2014 · 205 comments 路 May be fixed by #33294

Comments

@RyanCavanaugh
Copy link
Member

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

This comment has been minimized.

Copy link
Contributor

commented Jul 15, 2014

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

This comment has been minimized.

Copy link
Member

commented Jul 15, 2014

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

This comment has been minimized.

@philipbulley

This comment has been minimized.

Copy link
Contributor

commented Sep 1, 2014

馃憤

@RyanCavanaugh

This comment has been minimized.

Copy link
Member Author

commented Sep 2, 2014

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

This comment has been minimized.

Copy link

commented Sep 11, 2014

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

@brian428

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Oct 1, 2014

I totally agree with brain428

@fdecampredon

This comment has been minimized.

Copy link

commented Oct 2, 2014

@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

This comment has been minimized.

Copy link

commented Oct 2, 2014

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

This comment has been minimized.

Copy link
Member Author

commented Oct 2, 2014

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

This comment has been minimized.

Copy link
Contributor

commented Nov 19, 2014

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鈥攁lthough insanely useful鈥攊sn'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

This comment has been minimized.

Copy link

commented Nov 29, 2014

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

This comment has been minimized.

Copy link

commented Dec 13, 2014

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link
Contributor

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

This comment has been minimized.

Copy link

commented Jun 18, 2015

+1

@basarat

This comment has been minimized.

Copy link
Contributor

commented Jun 18, 2015

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

@ghost

This comment has been minimized.

Copy link

commented Jul 1, 2015

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?

@jhpratt

This comment has been minimized.

Copy link

commented Jul 27, 2019

Surely the document.all case could be special? It shouldn't require much extra code, just a few lines to check the object and property.

@ExE-Boss

This comment has been minimized.

Copy link
Contributor

commented Jul 27, 2019

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

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Contributor

commented Jul 27, 2019

@jhpratt But that currently goes against the聽TypeScript Design Non鈥慓oals.

Also, you鈥檇 still have to聽do聽foo聽===聽null聽|| foo聽===聽void聽0 for聽anything to聽which HTMLAllCollection could be聽assigned, eg.聽any聽or聽object, so聽I don鈥檛 think it鈥檚聽really worth聽it.

@jhpratt

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

commented Aug 12, 2019

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

@RyanCavanaugh

This comment has been minimized.

Copy link
Member Author

commented Aug 19, 2019

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link

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

This comment has been minimized.

Copy link
Member Author

commented Aug 24, 2019

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!

Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
You can鈥檛 perform that action at this time.