Optional chaining gotcha in Angular
The optional chaining operator behaves differently in Angular templates than it does in typescript
I was caught out by some unexpected behaviour when implementing an Angular component, recently. The component had an input typed number | undefined
like this:
class DownloadProgressComponent {
@Input() progress: number | undefined;
}
In the template for this component, the progress
input was used in an *ngIf
along these lines:
<ng-container *ngIf="progress !== undefined; else notStarted">
<!-- progress bar -->
</ng-container>
<ng-template #notStarted>
<!-- not started message -->
</ng-template>
The idea is pretty simple here; if the progress
input has a defined value (including possibly 0
) then we show a progress bar, otherwise we show some sort of "download not started" message.
All good so far, until finally in the template of the parent component, an expression using optional chaining was bound to the progress
input of this component:
<app-download-progress
[progress]="download?.progress"
></app-download-progress>
Before the user initiates a download, the download
object referenced here is undefined
. But when in this state, the progress bar was still being rendered in the progress component, and the notStarted
template was not.
Cue some significant head scratching.
What's going on?
A little bit of debugging revealed that progress
input was actually being set to null
, not undefined
as I expected.
As null !== undefined
is true
, the *ngIf
condition was always true
under all circumstances, and the notStarted
template was therefore never rendered.
It turns out, Angular implemented the optional chaining syntax for expressions in HTML templates (a.k.a., the "safe navigation operator") before it was part of ECMAScript. And when they did so, they chose to make the expression evaluate to null
when the left-hand side of the operator is nullish, not undefined
like ECMAScript later went on to use.
strictTemplates
This was particularly frustrating as the project concerned had the strictTemplates Angular compiler flag enabled, which is supposed to help avoid subtle type issues like this. Yet the compiler did not complain when an expression that could evaluate to null
was provided to an input whose type did not accept null
.
What's more, attempting to accommodate the behaviour by changing the type of the input to number | null
instead resulted in the Angular compiler now complaining (incorrectly) that we're attempting to assign a possibly undefined
value to a property that does not accept undefined
.
The workaround I settled for in the end was to use a non-strict equality check instead (i.e., *ngIf="progress != undefined"
). As this violated the eqeqeq
linting rule which requires the use of strict equality operators, I also had to configure the linting rule to explicitly allow non-strict equality checks against nullish literals with the following in .eslintrc.json
:
{
"files": ["*.html"],
"extends": ["plugin:@nrwl/nx/angular-template"],
"rules": {
"@angular-eslint/template/eqeqeq": [
"error",
{
"allowNullOrUndefined": true
}
]
}
}
Further reading
A small demonstration of the issue can be seen in this github repo.
An issue was reported to Angular in Dec 2019. At time of writing it is still open, so aligning with ECMAScript on this has not been ruled out, and hopefully this will be fixed soon.
Support
If you've found my blog useful, please consider buying me a coffee to say thanks:
Discuss
Comments or questions? Join the conversation on Twitter: