feat(compiler): narrow types of expressions used in *ngIf (#20702)
Structural directives can now specify a type guard that describes what types can be inferred for an input expression inside the directive's template. NgIf was modified to declare an input guard on ngIf. After this change, `fullTemplateTypeCheck` will infer that usage of `ngIf` expression inside it's template is truthy. For example, if a component has a property `person?: Person` and a template of `<div *ngIf="person"> {{person.name}} </div>` the compiler will no longer report that `person` might be null or undefined. The template compiler will generate code similar to, ``` if (NgIf.ngIfTypeGuard(instance.person)) { instance.person.name } ``` to validate the template's use of the interpolation expression. Calling the type guard in this fashion allows TypeScript to infer that `person` is non-null. Fixes: #19756? PR Close #20702
This commit is contained in:

committed by
Jason Aden

parent
e544742156
commit
e7d9cb3e4c
@ -81,6 +81,141 @@ describe('ng type checker', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('type narrowing', () => {
|
||||
const a = (files: MockFiles, options: object = {}) => {
|
||||
accept(files, {fullTemplateTypeCheck: true, ...options});
|
||||
};
|
||||
|
||||
it('should narrow an *ngIf like directive', () => {
|
||||
a({
|
||||
'src/app.component.ts': '',
|
||||
'src/lib.ts': '',
|
||||
'src/app.module.ts': `
|
||||
import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core';
|
||||
|
||||
export interface Person {
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'comp',
|
||||
template: '<div *myIf="person"> {{person.name}} </div>'
|
||||
})
|
||||
export class MainComp {
|
||||
person?: Person;
|
||||
}
|
||||
|
||||
export class MyIfContext {
|
||||
public $implicit: any = null;
|
||||
public myIf: any = null;
|
||||
}
|
||||
|
||||
@Directive({selector: '[myIf]'})
|
||||
export class MyIf {
|
||||
constructor(templateRef: TemplateRef<MyIfContext>) {}
|
||||
|
||||
@Input()
|
||||
set myIf(condition: any) {}
|
||||
|
||||
static myIfTypeGuard: <T>(v: T | null | undefined | false) => v is T;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MainComp, MyIf],
|
||||
})
|
||||
export class MainModule {}`
|
||||
});
|
||||
});
|
||||
|
||||
it('should narrow a renamed *ngIf like directive', () => {
|
||||
a({
|
||||
'src/app.component.ts': '',
|
||||
'src/lib.ts': '',
|
||||
'src/app.module.ts': `
|
||||
import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core';
|
||||
|
||||
export interface Person {
|
||||
name: string;
|
||||
}
|
||||
|
||||
@Component({
|
||||
selector: 'comp',
|
||||
template: '<div *my-if="person"> {{person.name}} </div>'
|
||||
})
|
||||
export class MainComp {
|
||||
person?: Person;
|
||||
}
|
||||
|
||||
export class MyIfContext {
|
||||
public $implicit: any = null;
|
||||
public myIf: any = null;
|
||||
}
|
||||
|
||||
@Directive({selector: '[my-if]'})
|
||||
export class MyIf {
|
||||
constructor(templateRef: TemplateRef<MyIfContext>) {}
|
||||
|
||||
@Input('my-if')
|
||||
set myIf(condition: any) {}
|
||||
|
||||
static myIfTypeGuard: <T>(v: T | null | undefined | false) => v is T;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MainComp, MyIf],
|
||||
})
|
||||
export class MainModule {}`
|
||||
});
|
||||
});
|
||||
|
||||
it('should narrow a type in a nested *ngIf like directive', () => {
|
||||
a({
|
||||
'src/app.component.ts': '',
|
||||
'src/lib.ts': '',
|
||||
'src/app.module.ts': `
|
||||
import {NgModule, Component, Directive, HostListener, TemplateRef, Input} from '@angular/core';
|
||||
|
||||
export interface Address {
|
||||
street: string;
|
||||
}
|
||||
|
||||
export interface Person {
|
||||
name: string;
|
||||
address?: Address;
|
||||
}
|
||||
|
||||
|
||||
@Component({
|
||||
selector: 'comp',
|
||||
template: '<div *myIf="person"> {{person.name}} <span *myIf="person.address">{{person.address.street}}</span></div>'
|
||||
})
|
||||
export class MainComp {
|
||||
person?: Person;
|
||||
}
|
||||
|
||||
export class MyIfContext {
|
||||
public $implicit: any = null;
|
||||
public myIf: any = null;
|
||||
}
|
||||
|
||||
@Directive({selector: '[myIf]'})
|
||||
export class MyIf {
|
||||
constructor(templateRef: TemplateRef<MyIfContext>) {}
|
||||
|
||||
@Input()
|
||||
set myIf(condition: any) {}
|
||||
|
||||
static myIfTypeGuard: <T>(v: T | null | undefined | false) => v is T;
|
||||
}
|
||||
|
||||
@NgModule({
|
||||
declarations: [MainComp, MyIf],
|
||||
})
|
||||
export class MainModule {}`
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('regressions ', () => {
|
||||
const a = (files: MockFiles, options: object = {}) => {
|
||||
accept(files, {fullTemplateTypeCheck: true, ...options});
|
||||
|
@ -1038,6 +1038,25 @@ describe('Collector', () => {
|
||||
expect(metadata).toBeUndefined();
|
||||
});
|
||||
|
||||
it('should collect type guards', () => {
|
||||
const metadata = collectSource(`
|
||||
import {Directive, Input, TemplateRef} from '@angular/core';
|
||||
|
||||
@Directive({selector: '[myIf]'})
|
||||
export class MyIf {
|
||||
|
||||
constructor(private templateRef: TemplateRef) {}
|
||||
|
||||
@Input() myIf: any;
|
||||
|
||||
static typeGuard: <T>(v: T | null | undefined): v is T;
|
||||
}
|
||||
`);
|
||||
|
||||
expect((metadata.metadata.MyIf as any).statics.typeGuard)
|
||||
.not.toBeUndefined('typeGuard was not collected');
|
||||
});
|
||||
|
||||
it('should be able to collect an invalid access expression', () => {
|
||||
const source = createSource(`
|
||||
import {Component} from '@angular/core';
|
||||
|
Reference in New Issue
Block a user