angular: Compile errors when using async pipe with enableIvy & strictNullChecks

šŸž bug report

Affected Package

@angular/compiler-cli, @angular/angular

Is this a regression?

Yes, this works when Ivy is not used.

Description

The problem is the async pipe returns T | null. My understand is Ivy uses code similar to Partial<Pick<ChildComponent, ā€˜propā€™>> to check property bindings in templates. That allows undefined, but not null. Similarly, ngForOf doesnā€™t allow null, so that also doesnā€™t work with async.

This causes templates that previously compiled without errors to fail.

    <!-- Must be built with strictNullChecks && enableIvy to see the errors below -->
    <ul>
      <!-- Type 'string[] | null' is not assignable to type 'string[] | Iterable<string> | undefined' -->
      <li *ngFor="let n of array$ | async">{{n}}</li>
    </ul>
    <!-- Type 'number | null' is not assignable to type 'number | undefined' -->
    <app-child [prop]="value$ | async"></app-child>

We use ngrx/store, so we use the async pipe a lot, and for the most part the observables from the store will always return a value immediately. But asyncā€™s return type means we have to deal with the null somehow even if it will never be emitted.

Iā€™ve come up with several workarounds, but I donā€™t like any of them very much. And even if this is seen as behaving as intended, documentation should be updated (e.g. the example in https://angular.io/api/common/NgForOf#local-variables wouldnā€™t work with strictNullChecks && enableIvy).

Workarounds:

  1. Using non null assertions [prop]=(value$ | async)!"

I donā€™t like this since I donā€™t want developers to get used to throwing it around causally.

  1. Use || to provide a default, *ngFor=ā€œlet i of (array$ | async) || []ā€

This works okay for the ngFor case, but doesnā€™t work for the prop case if one of the valid values is falsey, or if there is no good default value.

  1. Use ngIf with async, *ngIf=ā€œvalue$ | async as valueā€

This is probably a good choice for the cases where the async pipe would legitimately be null, but is just needlessly cluttering template with ng-contianer or other elements in the cases where the async pipe will never return null. It also doesnā€™t work when the pipe will return a falsey value.

  1. Create a ā€œpresentā€ pipe that throws if itā€™s input is null/undefined and converts T | null to just T

Alternatively create a currentValue pipe that combines async & present, which could limit the input to Observable<T> (since the other options would just cause it to throw anyway).

This works but it makes the template expressions longer, and if itā€™s not built into @angular/angular its more for new devs on the team to understand.

šŸ”¬ Minimal Reproduction

Iā€™ve created a repo showing this issue: https://github.com/james-schwartzkopf/test-template/blob/master/src/app/app.component.ts

šŸ”„ Exception or Error

See Above

šŸŒ Your Environment

Angular Version:


$ ng --version

     _                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / ā–³ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 8.2.0
Node: 10.15.1
OS: win32 x64
Angular: 8.2.0
... animations, cli, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router

Package                           Version
-----------------------------------------------------------
@angular-devkit/architect         0.802.0
@angular-devkit/build-angular     0.802.0
@angular-devkit/build-optimizer   0.802.0
@angular-devkit/build-webpack     0.802.0
@angular-devkit/core              8.2.0
@angular-devkit/schematics        8.2.0
@ngtools/webpack                  8.2.0
@schematics/angular               8.2.0
@schematics/update                0.802.0
rxjs                              6.4.0
typescript                        3.5.3
webpack                           4.38.0

Anything else relevant?


$ cat ./tsconfig.app.json
{
  "extends": "./tsconfig.json",
  "compilerOptions": {
    "outDir": "./out-tsc/app",
    "types": []
  },
  "files": [
    "src/main.ts",
    "src/polyfills.ts"
  ],
  "include": [
    "src/**/*.d.ts"
  ],
  "angularCompilerOptions": {
    "enableIvy": true
  }
}


$ cat ./tsconfig.json
{
  "compileOnSave": false,
  "compilerOptions": {
    "strictNullChecks": true,
    "baseUrl": "./",
    "outDir": "./dist/out-tsc",
    "sourceMap": true,
    "declaration": false,
    "downlevelIteration": true,
    "experimentalDecorators": true,
    "module": "esnext",
    "moduleResolution": "node",
    "importHelpers": true,
    "target": "es2015",
    "typeRoots": [
      "node_modules/@types"
    ],
    "lib": [
      "es2018",
      "dom"
    ]
  },
  "angularCompilerOptions": {
    "fullTemplateTypeCheck": true,
    "strictInjectionParameters": true
  }
}

About this issue

  • Original URL
  • State: closed
  • Created 5 years ago
  • Reactions: 12
  • Comments: 17 (11 by maintainers)

Most upvoted comments

@klemenoslaj Ivyā€™s template type checker is far more advanced than the level of type checking that was previously done for templates, and in general the errors it produces are accurate. The problem is that they may be too accurate for someoneā€™s liking, especially because of cases like ngFor together with the async pipe where there has always been a type mismatch that has not been surfaced before because the type checking was not as accurate as it is becoming now.

Is it now expected that all inputs in our applications accept null?

This is not the case. This issue in particular is about the async pipe, which includes null in its return type as it canā€™t differentiate based on the type of Observable if it will emit synchronously, or whether there may be a gap between the time of subscribing and the first value emission (in which case the async pipe has to return null, because it doesnā€™t have any other information)

The good news is that we recently introduced an internal template type checking option to effectively ignore typing issues around null and undefined. A public facing option will be added shortly, so that developers can disable certain areas of the template type checker. This is basically @pshuryginā€™s suggestion in https://github.com/angular/angular/issues/32051#issuecomment-536285449.

Disclaimer: I am not entitled to make official statements šŸ˜ƒ Iā€™m just enthusiastic about Ivy and helping out whenever I can.

So only ngFor was changed? This will still be an issue for @Inputs that donā€™t include null in their type?

Thanks @Splaktar, I agree. If any of you is still experiencing problems after updating to at least 9.0.0-next.10, please open a new issue.

If this issue is resolved byNgForOf getting fixed to accept the null in its typings that still means all the custom inputs we develop ourselves will have to accept null by default? I mean, all of themā€¦Do I even have to mention how cumbersome that is?

@Input() values?: number[] | null.

or even worse, solving the issue within templates

<component [values]="(valuesStream | async) || undefined"></component>

Angular getā€™s in our way here by preventing us writing our code the way we want it.

Question: There is probably a technical reason why async and some other pipes return null, because if not, is it really needed?

My reasoning: Subscribing to an observable directly will not give you null by default eitherā€¦you will get nothing until the actual emission happens.

Iā€™m having a similsar issues without using ngFor.

Iā€™m using ng-select component like this: <ng-select [(ngModel)]="city" [items]="cities$ | async">...</ng-select> Here the cities$ have (non-nullable) type Observable<City[]> and [items] binding of the ng-select accepts (any[]|undefined), but async pipe changes type of cities$ into (City[]|null), and so we have a compile error.

Another problem of the same category is with the number pipe, as it returns string|null. The code i have is <div [matTooltip]="value|number">...</div> where value is non-nullable number, but itā€™s type is converted into string|null, which causes compiler error with matTooltip binding accepting string|undefined.