angular / angular

Deliver web apps with confidence 🚀
https://angular.dev
MIT License
96.09k stars 25.43k forks source link

Align with the optional chaining spec #34385

Open mgechev opened 4 years ago

mgechev commented 4 years ago

🚀 feature request

Relevant Package

@angular/compiler

Description

Optional chaining[1] reached stage 4. We've been supporting similar syntax in templates for a while now, calling it the "safe navigation operator"[2]. For simplicity and smaller payload, we can consider aligning with the spec in future versions of the framework.

There are a couple of semantical and syntactical differences between optional chaining and safe navigation.

Syntax

Optional chaining has the following syntax:

obj?.prop       // optional static property access
obj?.[expr]     // optional dynamic property access
func?.(...args) // optional function or method call

Safe navigation supports only direct property access. Optional chaining supports this, as well as, method calls and function calls. Function calls are particularly useful in iterators:

iterator.return?.()

Semantics

With optional chaining, the expression a?.b will be translated to a == null ? undefined : a.b. In Angular, the semantics of the same expression would be null == a ? null : a.b.

If a is null or undefined, the expression typeof a?.b would evaluate to "object" with optional chaining and "undefined" in Angular's safe navigation operator.

Except the mentioned difference above, method calls are compiled similarly:

a?.b()
a == null ? undefined : a.b()

In both, optional chaining and safe navigation in templates, stacking the operators is translated the same way: (a?.b).c?.d becomes null == a ? null : null == a.b.c ? null : a.b.c.d.

Another difference seems to be the way parentheses are handled. The optional chaining spec defines that null==e.foo?null:e.foo.b.c should be translated to (a == null ? undefined : a.b).c. In Angular the same expression translates to null == a ? null : a.b.c.

PS: looks like the last issue is fixed by https://github.com/angular/angular/pull/34221.


[1] Optional chaining spec https://github.com/tc39/proposal-optional-chaining [2] Safe navigation https://angular.io/guide/template-syntax#safe-navigation-operator

thw0rted commented 4 years ago

Just to check, does this cover the case of array-index optional chaining (arr?.[0]) in templates? I get an error when I try to use that today, though I'm not sure if it's a problem Angular itself or just the language service.

rodolfojnn commented 4 years ago

It would be very interesting to use optional chaining in arrays, following the TS spec:

In Angular Templates, arr?.[0] causes a:

zone-evergreen.js:659 Unhandled Promise rejection: Template parse errors: Parser Error: Unexpected token [, expected identifier or keyword at column 4 in [arr?.[0]] in ng:///ProdutoCadComponent/template.html@996:64

craftmaster2190 commented 4 years ago

Arrays/Indexes are safely navigated in templates but not in the way one would expect: In TypeScript: foo?.bar?.[0]?.quxx In Angular Templates: foo?.bar[0]?.quxx See https://github.com/angular/angular/issues/13254 and https://github.com/angular/angular/commit/f31c9470fae06adc69b2b665773bfc0a8a10d10e

craftmaster2190 commented 4 years ago

TL;DR: Angular Templates resolves to null in safe navigation and TypeScript resolves to undefined in optional chaining.

Consider the following:

Component Typescript

public preferences: {email: string};
public getPreferencesEmail() {
    return this.preferences?.email;
}
public ngOnInit() {
    this.preferencesService.subscribe(preferences =>
        (this.preferences = preferences));
}

Example A HTML

<div *ngIf="preferences?.email === undefined; else emailInput">Loading...</div>
<ng-template #emailInput>
   <input id="email" [value]="preferences.email">
</ng-template>

Example B HTML

<div *ngIf="getPreferencesEmail() === undefined; else emailInput">Loading...</div>
<ng-template #emailInput>
   <input id="email" [value]="getPreferencesEmail()">
</ng-template>

In Example A, the loading div does not show because Angular Templates evaluates the safe-navigation to null.

In Example B, the loading div does show because TypeScript evaluates the optional chaining to undefined.

If undefined means "Loading", null means "Not set by user" and anything else is "a user-set value"; then angular's safe navigation introduces a bug in our code.

This can be even more confusing given that preferences is undefined and the Angular's safe navigation resolves preferences?.email to null.

johnwest80 commented 4 years ago

Arrays/Indexes are safely navigated in templates but not in the way one would expect: In TypeScript: foo?.bar?.[0]?.quxx In Angular Templates: foo?.bar[0]?.quxx See #13254 and f31c947

The problem with angular's syntax is that there's no way to check that the array isn't undefined or null. Yes, what you show will check to see if the array has that indexed item, but if I want to know that the property is in fact initialized, I have to do

foo?.bar && foo?.bar[0]?

It would be much better to use optional chaining and just use

foo?.bar?.[0]?

ajafff commented 4 years ago

Another thing to note: since TypeScript 3.9 non-null-assertions don't end the optional chain

foo?.bar!.baz;

// ts@<3.9
(foo?.bar).baz; // asserts that `foo?.bar` is non-null - which obviously doesn't make sense

// ts@>=3.9
(foo?.bar.baz); // asserts that if `foo` is non-null, its `bar` property will also be non-null
Sampath-Lokuge commented 4 years ago

Hi,

For me it shows this:

Parser Error: Unexpected token [, expected identifier or keyword at column 33 in [

                    {{userItemModel?.item?.priceList?.[0].sellerUrl}}
                ] in 

SOF Link: https://stackoverflow.com/questions/64104994/optional-chaining-is-not-working-cannot-read-property-0-of-undefined

adeel55 commented 3 years ago

In my case

this.property?.UserProperties?.[0].id

Display error:

Template parse errors: Parser Error: Unexpected token [, expected identifier or keyword at column 32 in [this.property?.UserProperties?.[0].id] in bmp.component.html@219:84

snebjorn commented 3 years ago

Arrays/Indexes are safely navigated in templates

bar[0]?.quxx may be safely navigated runtime. But compile time it's not.

Both the language service and the compiler errors on the above.

[foo]="bar[0]?.quxx"
       ~~~~~~
Error: Object is possibly 'null'. ngtsc(2531)

This makes the "safe navigated in templates" irrelevant as the compiler/type checker doesn't allow it.

The proper ES syntax (bar?.[0]?.quxx) also makes it clear what object can be null|undefined. In the above example both bar and bar[0] can be null|undefined.

Christoph142 commented 3 years ago

This leads to very inconsistent behavior. I used the exact same code both in *ngIf and if in code: foo?.bar <= 0 If foo is null, the result is true in templates and false in code. 🙈

thw0rted commented 3 years ago

@petebacondarwin since you're eyes-on here, and it's been quite some time since this issue was really active, do you think you could get a status for this from the team? Maybe assign a priority?

petebacondarwin commented 3 years ago

@thw0rted - we definitely want to move to ECMAScript compliant behaviour, but since this would be a breaking change, we would also need to consider how to migrate users and it would have to happen in a major release. This is unlikely to happen for v13 now, but I will raise it in our next framework sync up meeting in two weeks.

johnwest80 commented 3 years ago

since there are finite rules around how angular does optional chaining, it would be a relatively easy regex replace rule for the most part, right? i know, it's always easy to say something's easy :)

petebacondarwin commented 3 years ago

I think it is definitely possible to write a migration for these cases, but I don't think it is trivial. So we would need to plan the work alongside prioritising other work.

petebacondarwin commented 3 years ago

This is being tracked in the aggregate issue of #43485 for which we need to create a project proposal.

SetoKaiba commented 2 years ago

image

I tried to write a webpage for OBS Studio. But the optional chaining is not supported. Is there a way to fix this?

SetoKaiba commented 2 years ago

Optional chaining is a es10 feature. Why must me set the target to es5? If I set the target to es6 or above. The optional chaining is still in the compiled js.

SetoKaiba commented 2 years ago

https://github.com/angular/angular/blob/b5ab7aff433a67cddaa55e621d17b1a1b07b57c2/packages/common/src/location/location_strategy.ts#L176-L178 Why this ?. is kept even I set target to es2015? It will only be remove with es5 target.

thw0rted commented 2 years ago

@SetoKaiba you need to double check your settings. Compiling with target: "ES2015" downlevels optional chaining for me. Try this simple Playground example with a target of ES2015 (under the TS Config dropdown menu). I can step all the way up to ES2019 and it still downlevels, then at ES2020 it's preserved in the emit. At any rate, this isn't Angular's fault.

SetoKaiba commented 2 years ago

@thw0rted I confirmed that it's caused by the library code is not compiled against ES2015. @alan-agius4 suggested a fix for this. Please check #22270.

thw0rted commented 2 years ago

Are you sure that's the right issue number? Anyway, Angular is supposed to dynamically compile against a target to match your project -- I hadn't thought about it but I guess that could be bugged / broken in some circumstances. Anyway, even if it is Angular's "fault", it doesn't belong on this issue, which is specifically about supporting optional chaining syntax in templates.

SetoKaiba commented 2 years ago

@thw0rted Sorry. It's moved to another project. This is the right issue. https://github.com/angular/angular-cli/issues/22270

alan-agius4 commented 2 years ago

@SetoKaiba, your issue in not related to this issue.

SetoKaiba commented 2 years ago

@thw0rted Sorry. This issue mentioning optional chaining operator. I thought it's related. It should be related to the issue I create,

alan-agius4 commented 2 years ago

This feature request is about optional chaining schematics in templates.

daiscog commented 2 years ago

The really annoying thing about this is that it breaks the type system, because compile-time type checks think foo?.value in a template is T | undefined, whereas it's actually T | null.

For example, if you have a component with an Input of a type that allows undefined values but does not allow null values, then using optional chaining when binding to that input may result in it being assigned the value null, but the compiler does not complain about this, even with strictTemplates enabled.

What's worse, if you change the input type to allow null and disallow undefined, then the compiler fails as it thinks the optional chaining syntax may return undefined, even though it actually returns null:

 Type 'string | undefined' is not assignable to type 'string | null'.
   Type 'undefined' is not assignable to type 'string | null'

        [value]="foo?.value"
         ~~~~~

A demonstration can be seen in this project.

amakhrov commented 2 years ago

@daiscog that's true. it's also tracked in a separate issue: https://github.com/angular/angular/issues/37622

rodolfojnn commented 2 years ago

Another workaround is to use the "slice" or the "at" method:

array?.slice(0, 1)[0]

or 

array?.at(0) // Only for es2022+
Killerbear commented 10 months ago

we just run into this issue and would love to see a fix for it. ❤️