question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Use of `null` in `async` pipe has become problematic with NG12, TS4 and strict type checks

See original GitHub issue

Which @angular/* package(s) are relevant/releated to the feature request?

@angular/common

Description

I’ve recently been using Angular 12 for the majority of my Angular projects. I was previously used to non-strict mode TypeScript, which is more convenient, but it definitely allows more little issues to slip through. I appreciate strict mode, and generally prefer it, however there are some things about Angular that make strict mode extremely tedious in certain common use cases.

Notably, the async pipe can emit one of three values: undefined, null, or the type of the stream you are piping. The pipe seems to emit null, I think, as an initial value? The problem here is that it basically forces all presentational components to use inputs defined like this:

export class BasicPresenter {
  @Input() someObject?: SomeObject | null;
  @Input() someArray: SomeObject[] | null = [];
}

I rarely ever use nulls in my own code. My code usually relies on things either being undefined (not present), or defined and present. The rare case where I may use nulls is with some form fields where I explicitly want data written to a datastore on a server somewhere to actually be null, but that usually applied to properties of my entities…the entities themselves are either undefined or defined, never null.

The fact that async returns null forces me to add | null all throughout my codebase, potentially hundreds or thousands of components, which would not otherwise have to deal with null at all. It affects every stream of every kind, so even if you have streams that are basic primitives, especially streams that never have nulls in them, you still have to deal with string | null, or number | null, etc.

It should also be noted that if null is legitimately NOT a value you WANT to be allowed from a stream or to an input, the fact that the async pipe introduces it forces types to be expanded, thus potentially allowing inappropriate usage of your custom components. Further, null is not a value that will cause, say, default parameters to trigger…if you pass an input on a child component through a pipe that has parameters with default values, if the async pipe starts out with null values, and those values are passed to a pipe in the template, the defaults would not be used, null would be used instead. This in a sense makes null an infectious type that has to be dealt with in more and more code. One of the benefits of using strict mode typescript…is to make the need and use of null EXPLICIT. The async pipes expansion of the types of the Observable streams being piped eliminates a lot of this benefit for child components…and often other code they may have to consume.

For an enterprise scale project with potentially thousands of components, each of which may on average have 3, 5, 10 inputs depending on the nature of the project, that could be many thousands more instances where I have to deal with | null in my code. Its extremely tedious.

I’m honestly not really sure why null comes into play with the AsyncPipe. The API documentation doesn’t mention null at all. Looking at the source code, it appears that null is the initial value for some things, when I think undefined would in fact be more appropriate.

Proposed solution

I propose that the async pipe should only deal with the explicit type defined for the Observable that is being piped. If an Observable is strictly of type SomeObject, then the type for values emitted by async should be SomeObject. By using null internally, then an expansion of the type occurs, when it could potentially be very much undesired. Expansion of the type to include null and undefined requires that downstream code deal with those values, which increases the complexity of that code.

Since the non-nullish assertion operator has been largely shunned by the community, asserting that something cannot be null is usually caught by linters and compilation is impossible without either disabling rules, or commenting them out for each use case (again, tedious and really shouldn’t be necessary.) Non-nullish assertion could be used, but then if it IS appropriate that an input could potentially be undefined, non-nullish assertion cannot actually be used, and you would again be stuck adding | null to your code when its inappropriate.

Maintaining the type narrowness of the original observable seems reasonable. Observables of a particular type may never emit anything. If an observable never emits a value, then no attempt should be made to set the given property of the child component. This would avoid the need to say, initially set all inputs to null, or even undefined. There was never a value emitted by the stream, thus there would never be a value input into the child component, and no event (i.e. no ngOnChanges call) for that input.

Alternatives considered

Alternatively, if for some reason an initial value MUST be input into every child component, perhaps the async pipe could maintain the narrowest possible type of SomeObject | undefined. This would eliminate the need to add | null to every input, and instead rely solely on just adding the optional property marker ?, or defaulting the initial value of the property:

  @Input() someObject?: SomeObject;
  @Input() someArray: SomeObject[] = [];

Which comes out a lot cleaner and less tedious than including null as a valid type value.

Issue Analytics

  • State:open
  • Created 2 years ago
  • Reactions:58
  • Comments:28 (5 by maintainers)

github_iconTop GitHub Comments

6reactions
pkozlowski-opensourcecommented, Oct 5, 2022

A short update on this issue, that discusses both the real-life pain-point / problem as well as possible solutions.

Let’s start with the problem first, which is the fact that the type of the async pipe transform is always widened to include null. The null inclusion comes from the fact that an observable might not have any value when subscribing. And “not having a value synchronously” is certainly true for promises (which are supported by the async pipe in addition to observables.

Given the stated problem, we can approach it from 3 different angles:

  • assume that we are always dealing with asynchronous values (as the pipe name implies!) and include “no value before subscription” in the T. If we take this stance then the problem boils down to the choice between null and undefined for “no value before subscription”. Historically the implementation choice was to include null but it causes real-life issues with @Input bindings - as described in this issue we can’t declare inputs as @Input foo?: T and are forced to do @Input foo?: T|null. This is is sub-optimal and choosing undefined would address some of the pain. Sadly, this would be a very breaking change as noted in https://github.com/angular/angular/issues/16982#issuecomment-769471368
  • assume that we are dealing with a stream that “always have a value on subscription” (ex. BehaviorSubject) and indicate it via an additional argument to the async pipe (ex. sync: boolean discussed in https://github.com/angular/angular/pull/47608#issuecomment-1266120178). This stance has 2 issues:
    • it doesn’t work for promises;
    • applying an operator to a “sync” stream can easily turn it into an “async” one;
  • add the notation of a “initial value” to the async pipe - this is what we’ve attempted in #47608 but it has 2 problems as well:
    • there are cases where it is not obvious how to construct an “initial value” (thnx for pointing this out @JoostK );
    • there is no huge advantage of the “initial value” over doing <test-cmp [foo]="(obs$ | async) ?? initialExp"></test-cmp>

We’ve discussed all those options in the team and don’t feel like any of the approaches is a “winner” here - we either end up with a sub-optimal solution and / or require costly migration.

At this point we do recognize the problem but don’t have a solution that we would be happy with. Introducing a separate pipe for “always have a value” observables (and dropping promises support) might be an option but this would fragment the Angular ecosystem by providing 2 ways of doing the same thing.

6reactions
hiepxanhcommented, Jan 2, 2022

I have 160 * 4 = 640 component file with about 2 or 3 property per component. it will be: 640 * 3 = 1920 modification… or n place have async pipe. I dont know which path will help me finish this task faster. I think people can save me by allow this to fix in the next release. today is 2022/01/02 I have 20 days to do this. While waiting this feature I will take some coffee to help me 😭

Read more comments on GitHub >

github_iconTop Results From Across the Web

angular and async pipe typescript complains for null
Disable strict null checks in Angular templates completely. When strictTemplates is enabled, it is still possible to disable certain aspects of ...
Read more >
Jon Rista on Twitter: "I've started a feature request for Angular, to ...
With strict nullish typing, the current design of AsyncPipe forces inputs to ... `async` pipe has become problematic with NG12, TS4 and strict...
Read more >
Initial Null Problem of AsyncPipe and async data-binding
Angular's AsyncPipe is a useful feature for template binding of asynchronous data, but it has a big problem since the beginning.
Read more >
cannot spyon on a primitive value undefined given jest angular
I'm trying to use spyOn to spy on functions. But, I get the ... of `null` in `async` pipe has become problematic with...
Read more >
Async Pipe Is Broken in Angular - YouTube
Learn why async pipe is broken and how to fix this problem. In last versions of Angular default value of async pipe is...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found