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

Align with the optional chaining spec #34385

Open
mgechev opened this issue Dec 12, 2019 · 30 comments
Open

Align with the optional chaining spec #34385

mgechev opened this issue Dec 12, 2019 · 30 comments
Labels
area: compiler Issues related to `ngc`, Angular's template compiler area: core Issues related to the framework runtime core: binding & interpolation Issue related to property/attribute binding or text interpolation feature Issue that requests a new feature
Milestone

Comments

@mgechev
Copy link
Member

mgechev commented Dec 12, 2019

🚀 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 #34221.


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

@AndrewKushnir AndrewKushnir added area: compiler Issues related to `ngc`, Angular's template compiler feature Issue that requests a new feature labels Dec 12, 2019
@ngbot ngbot bot modified the milestone: Backlog Dec 12, 2019
@thw0rted
Copy link

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

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

@pkozlowski-opensource pkozlowski-opensource added area: core Issues related to the framework runtime core: binding & interpolation Issue related to property/attribute binding or text interpolation labels May 8, 2020
@craftmaster2190
Copy link

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

@craftmaster2190
Copy link

craftmaster2190 commented May 8, 2020

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

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

ajafff commented Jun 17, 2020

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

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

adeel55 commented Dec 26, 2020

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

snebjorn commented Jun 2, 2021

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

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

thw0rted commented Sep 3, 2021

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

petebacondarwin commented Sep 3, 2021

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

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

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

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

@SetoKaiba

This comment has been minimized.

@SetoKaiba

This comment has been minimized.

@SetoKaiba

This comment has been minimized.

@thw0rted

This comment has been minimized.

@SetoKaiba

This comment has been minimized.

@thw0rted

This comment has been minimized.

@SetoKaiba

This comment has been minimized.

@alan-agius4

This comment has been minimized.

@SetoKaiba

This comment has been minimized.

@alan-agius4

This comment has been minimized.

@daiscog
Copy link

daiscog commented Mar 10, 2022

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

@daiscog that's true. it's also tracked in a separate issue: #37622

@rodolfojnn
Copy link

rodolfojnn commented Aug 24, 2022

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

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

or 

array?.at(0) // Only for es2022+

@Killerbear
Copy link

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

@thetric
Copy link

thetric commented Nov 15, 2024

Sadly the new control flow syntax did not change the behavior :(

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
area: compiler Issues related to `ngc`, Angular's template compiler area: core Issues related to the framework runtime core: binding & interpolation Issue related to property/attribute binding or text interpolation feature Issue that requests a new feature
Projects
None yet
Development

No branches or pull requests