angular and async pipe typescript complains for null
Asked Answered
G

5

15

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;
}
Garvy answered 21/4, 2021 at 6:39 Comment(5)
I won't do that boolean | null, that looks not correct to solve your issue. What is the exact compiler error you get?Cynde
I'm not sure if ngrx supports something like this syntax this.store.select<Data[]>(data); to get a typed result. What you at least should be able to do is: this.store.select(data) as Observable<Data[]>Cynde
it is the async pipe which returns boolean | null, i think this is the issue hereGarvy
How is the loading$ observable constructed? Does it have an initial value of true?Spotted
angular.io/guide/template-typecheck#strict-null-checksCerda
S
26

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.

Savonarola answered 10/6, 2021 at 14:35 Comment(2)
The last line in the solution #3 should be: private _rowData: Data[] = [];Quincey
This is gross... Angular could get their shtuff together. The main reason why this is happening is because if you have an empty observable Angular will render it as null.Desman
M
2

Modify your code snippet as below. should fix null assignment error.

 <app-presentational 
   [rowData]="(data$ | async)!" 
   [loading]="(loading$ | async)!" 
   ...
Myel answered 15/6, 2021 at 4:53 Comment(0)
S
0

I can't tell from just the information you provided, but it looks the component just needs null handling when the Observable hasn't provided a value

<app-presentational 
  [rowData]="data$ | async" 
  [loading]="(loading$ | async) || false"
/>
Spotted answered 8/6, 2021 at 21:14 Comment(1)
Not sure if "(data$ | async) || []" is needed as the complier is only complaining about the boolean.Spotted
C
0

The Angular documentation suggests using the non-null assertion operator !. An example of this would be:

<user-detail [user]="(user$ | async)!"></user-detail>

Using the non-null assertion operator indicates that you are confident a value will always be present, which overrides TypeScript's null-checking behavior.

However, this approach is not always appropriate. It's best used when you can guarantee a value from the start, such as when defining observables with of().

Another approach could be turn your component inputs into observables, or to accept null values, but now we're making 'dumb' components smarter.

Here are some general guidelines I stick to with examples:

1. Non-null assertion

If you are certain there will be a value from the beginning, use the non-null assertion operator:

<user-detail [user]="(user$ | async)!"></user-detail>

This is appropriate when you know user$ will always emit a value, perhaps because it is initialized with a value immediately.

2. If / else

If you are waiting for a value, implement a loading state and render the component once the value is available:

<ng-container *ngIf="user$ | async as user; else loading">
  <user-detail [user]="user"></user-detail>
</ng-container>
<ng-template #loading>
  <p>Loading...</p>
</ng-template>

In this case, ngIf checks if user$ has emitted a value. If it hasn't, the loading template is displayed.

3. Fallback value

If you need to render the component even while waiting for a value, provide a fallback:

<user-detail [user]="(user$ | async) || defaultUser"></user-detail>

Here, defaultUser is used as a fallback value until user$ emits a value. This approach is useful when you want to ensure the component is always rendered, even if the actual data is not yet available.

By following these rules and using the appropriate examples, you can ensure that your application handles values and loading states more effectively.

Chelseachelsey answered 18/6, 2024 at 15:49 Comment(0)
H
-1

Your Store State initialy is undefined that's why you getting null or you didn't declared the Store State object

  1. try having initial values:
export class PresentComponent {
  @Input()
  rowData: Data[] = [];

  @Input()
  loading = false;

Also for .select you are selecting part of the state it works with strings: try converting them to string

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

you can also do with multiples when going inside objects in state.

this.store.select('name1', 'name2', ...)
  1. But still it would be probably null because your initial state. You can try without the | async so this will never be null
export class EventsComponent implements OnInit {
  data: Data[] = [];
  loading = false;

  constructor(private store: Store<{ data: Data[], loading: boolean}>) {
  }

  ngOnInit(): void {
    // you can do one by one
    this.store.select('data')
      .subscribe(next => this.data = next);

    this.store.select('loading')
      .subscribe(next => this.loading = next);

    // or do

    // all together
    this.store.subscribe(next => {
      this.data = next.data;
      this.loading = next.loading;
    });
  }
}
Homophonic answered 8/6, 2021 at 20:54 Comment(0)

© 2022 - 2025 — McMap. All rights reserved.