fix(ivy): narrow `NgIf` context variables in template type checker (#35125)

When the `NgIf` directive is used in a template, its context variables
can be used to capture the bound value. This is typically used together
with a pipe or function call, where the resulting value is captured in a
context variable. There's two syntax forms available:

1. Binding to `NgIfContext.ngIf` using the `as` syntax:
```html
<span *ngIf="(user$ | async) as user">{{user.name}}</span>
```

2. Binding to `NgIfContext.$implicit` using the `let` syntax:
```html
<span *ngIf="user$ | async; let user">{{user.name}}</span>
```

Because of the semantics of `ngIf`, it is known that the captured
context variable is non-nullable, however the template type checker
would not consider them as such and still report errors when
`strictNullTypes` is enabled.

This commit updates `NgIf`'s context guard to make the types of the
context variables non-nullable, avoiding the issue.

Fixes #34572

PR Close #35125
This commit is contained in:
JoostK 2020-02-03 19:30:50 +01:00 committed by atscott
parent 208ef7bd62
commit 40039d8068
3 changed files with 53 additions and 3 deletions

View File

@ -232,7 +232,9 @@ export class NgIf<T = unknown> {
* The presence of this method is a signal to the Ivy template type-check compiler that the
* `NgIf` structural directive renders its template with a specific context type.
*/
static ngTemplateContextGuard<T>(dir: NgIf<T>, ctx: any): ctx is NgIfContext<T> { return true; }
static ngTemplateContextGuard<T>(dir: NgIf<T>, ctx: any): ctx is NgIfContext<NonNullable<T>> {
return true;
}
}
/**

View File

@ -70,7 +70,7 @@ export declare class NgIf<T = unknown> {
ngIfThen: TemplateRef<NgIfContext<T>> | null;
constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext<T>>);
static ngTemplateGuard_ngIf: 'binding';
static ngTemplateContextGuard<T>(dir: NgIf<T>, ctx: any): ctx is NgIfContext<T>;
static ngTemplateContextGuard<T>(dir: NgIf<T>, ctx: any): ctx is NgIfContext<NonNullable<T>>;
static ɵdir: i0.ɵɵDirectiveDefWithMeta<NgIf<any>, '[ngIf]', never, {'ngIf': 'ngIf'}, {}, never>;
}
@ -805,6 +805,54 @@ export declare class AnimationEvent {
env.driveMain();
});
it('should check usage of NgIf when using "let" to capture $implicit context variable', () => {
env.tsconfig({strictTemplates: true});
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'test',
template: '<div *ngIf="user; let u">{{u.name}}</div>',
})
class TestCmp {
user: {name: string}|null;
}
@NgModule({
declarations: [TestCmp],
imports: [CommonModule],
})
class Module {}
`);
env.driveMain();
});
it('should check usage of NgIf when using "as" to capture `ngIf` context variable', () => {
env.tsconfig({strictTemplates: true});
env.write('test.ts', `
import {CommonModule} from '@angular/common';
import {Component, NgModule} from '@angular/core';
@Component({
selector: 'test',
template: '<div *ngIf="user as u">{{u.name}}</div>',
})
class TestCmp {
user: {name: string}|null;
}
@NgModule({
declarations: [TestCmp],
imports: [CommonModule],
})
class Module {}
`);
env.driveMain();
});
it('should check basic usage of NgFor', () => {
env.write('test.ts', `
import {CommonModule} from '@angular/common';

View File

@ -236,7 +236,7 @@ export declare class NgIf<T = unknown> {
set ngIfThen(templateRef: TemplateRef<NgIfContext<T>> | null);
constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext<T>>);
static ngTemplateGuard_ngIf: 'binding';
static ngTemplateContextGuard<T>(dir: NgIf<T>, ctx: any): ctx is NgIfContext<T>;
static ngTemplateContextGuard<T>(dir: NgIf<T>, ctx: any): ctx is NgIfContext<NonNullable<T>>;
}
export declare class NgIfContext<T = unknown> {