Search code examples
angularngrx

angular and async pipe typescript complains for null


I have the following container component

export class EventsComponent {
  data$: Observable<Data[]> = this.store.select(data);
  loading$: Observable<boolean> = this.store.select(loading);
}

And bind the observables via | async to the presentational component:

 <app-presentational 
   [rowData]="data$ | async" 
   [loading]="loading$ | async" 
   ...
export class PresentComponent {
  @Input()
  rowData: Data[];

  @Input()
  loading: boolean;
}

However, the TS compiler always complains that the async pipe may return null.

Update, this is the exact error i get

Type 'boolean | null' is not assignable to type 'boolean'.
  Type 'null' is not assignable to type 'boolean'.ngtsc(2322)

So do I really have to change all my @Input() to this?

export class PresentComponent {
  @Input()
  rowData: Data[] | null;

  @Input()
  loading: boolean | null;
}

Solution

  • As Angular docs points here:

    There are two potential workarounds to the above issues:

    In the template, include the non-null assertion operator ! at the end of a nullable expression, such as <user-detail [user]="user!" />.

    In this example, the compiler disregards type incompatibilities in nullability, just as in TypeScript code. In the case of the async pipe, note that the expression needs to be wrapped in parentheses, as in <user-detail [user]="(user$ | async)!" />.

    Disable strict null checks in Angular templates completely.

    When strictTemplates is enabled, it is still possible to disable certain aspects of type checking. Setting the option strictNullInputTypes to false disables strict null checks within Angular templates. This flag applies for all components that are part of the application.

    But, instead of using the non-null assertion operator or even disabling the Angular strict checks, you could either:

    1. use nullish coalescing operator (??) - available in Angular 12+:

    TS:

    @Component({
      template: ``,
    })
    export class PresentationalComponent {
      @Input() loading: boolean;
      @Input() rowData: Data[];
    }
    

    HTML:

    <app-presentational
      [loading]="(loading$ | async) ?? false"
      [rowData]="(rowData$ | async) ?? []"
    ></app-presentational>
    
    1. use ngAcceptInputType_*:

    TS:

    @Component({
      template: ``,
    })
    export class PresentationalComponent {
      static ngAcceptInputType_loading?: boolean | null;
      // Note that if you have `@angular/cdk` installed you can use this instead:
      // static ngAcceptInputType_loading: BooleanInput;
      static ngAcceptInputType_rowData?: Data[] | null;
    
      @Input() loading: boolean;
      @Input() rowData: Data[];
    }
    

    HTML:

    <app-presentational
      [rowData]="rowData$ | async"
      [loading]="loading$ | async"
    ></app-presentational>
    
    1. TS 4.3+: different types for getter and setter:

    TS:

    @Component({
      template: ``,
    })
    export class PresentationalComponent {
      @Input()
      // Note that if you have `@angular/cdk` installed you can use `BooleanInput` instead.
      set loading(loading: boolean | null | undefined) {
        this._loading = loading ?? false;
      }
      get loading(): boolean {
        return this._loading;
      }
      private _loading: Data[];
    
      @Input()
      set rowData(rowData: Data[] | null | undefined) {
        this.rowData = rowData ?? [];
      }
      get rowData(): Data[] {
        return this._rowData;
      }
      private _rowData: Data[] = [];
    }
    

    Note that nowadays you should prefer to use option 3 rather than the 2, as input setter coercion fields are being deprecated.