Dai Codes

A software engineerʼs blog

Menu
Back
A close-up picture of some links in a large, rusty chain

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.

Discuss

Comments or questions? Join the conversation on Twitter: