From 02958c07f60ca6cd951b48cd37f50cc23d220b82 Mon Sep 17 00:00:00 2001 From: crisbeto Date: Fri, 29 Nov 2019 23:11:28 +0100 Subject: [PATCH] fix(common): reflect input type in NgIf context (#33997) Fixes the content of `NgIf` being typed to any. Fixes #31556. PR Close #33997 --- packages/common/src/directives/ng_if.ts | 34 +++++++++------ .../test/ngtsc/template_typecheck_spec.ts | 42 +++++++++++++++++-- packages/core/test/render3/common_with_def.ts | 2 +- tools/public_api_guard/common/common.d.ts | 17 ++++---- 4 files changed, 70 insertions(+), 25 deletions(-) diff --git a/packages/common/src/directives/ng_if.ts b/packages/common/src/directives/ng_if.ts index 92f8764a58..3520fd0ead 100644 --- a/packages/common/src/directives/ng_if.ts +++ b/packages/common/src/directives/ng_if.ts @@ -149,14 +149,14 @@ import {Directive, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef, ɵstri * @publicApi */ @Directive({selector: '[ngIf]'}) -export class NgIf { - private _context: NgIfContext = new NgIfContext(); - private _thenTemplateRef: TemplateRef|null = null; - private _elseTemplateRef: TemplateRef|null = null; - private _thenViewRef: EmbeddedViewRef|null = null; - private _elseViewRef: EmbeddedViewRef|null = null; +export class NgIf { + private _context: NgIfContext = new NgIfContext(); + private _thenTemplateRef: TemplateRef>|null = null; + private _elseTemplateRef: TemplateRef>|null = null; + private _thenViewRef: EmbeddedViewRef>|null = null; + private _elseViewRef: EmbeddedViewRef>|null = null; - constructor(private _viewContainer: ViewContainerRef, templateRef: TemplateRef) { + constructor(private _viewContainer: ViewContainerRef, templateRef: TemplateRef>) { this._thenTemplateRef = templateRef; } @@ -164,7 +164,7 @@ export class NgIf { * The Boolean expression to evaluate as the condition for showing a template. */ @Input() - set ngIf(condition: any) { + set ngIf(condition: T) { this._context.$implicit = this._context.ngIf = condition; this._updateView(); } @@ -173,7 +173,7 @@ export class NgIf { * A template to show if the condition expression evaluates to true. */ @Input() - set ngIfThen(templateRef: TemplateRef|null) { + set ngIfThen(templateRef: TemplateRef>|null) { assertTemplate('ngIfThen', templateRef); this._thenTemplateRef = templateRef; this._thenViewRef = null; // clear previous view if any. @@ -184,7 +184,7 @@ export class NgIf { * A template to show if the condition expression evaluates to false. */ @Input() - set ngIfElse(templateRef: TemplateRef|null) { + set ngIfElse(templateRef: TemplateRef>|null) { assertTemplate('ngIfElse', templateRef); this._elseTemplateRef = templateRef; this._elseViewRef = null; // clear previous view if any. @@ -225,14 +225,22 @@ export class NgIf { * narrow its type, which allows the strictNullChecks feature of TypeScript to work with `NgIf`. */ static ngTemplateGuard_ngIf: 'binding'; + + /** + * Asserts the correct type of the context for the template that `NgIf` will render. + * + * 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(dir: NgIf, ctx: any): ctx is NgIfContext { return true; } } /** * @publicApi */ -export class NgIfContext { - public $implicit: any = null; - public ngIf: any = null; +export class NgIfContext { + public $implicit: T = null !; + public ngIf: T = null !; } function assertTemplate(property: string, templateRef: TemplateRef| null): void { diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index b35ea5cc05..e6e6dcccd7 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -64,10 +64,19 @@ export declare class NgForOf> implements DoCheck { static ɵdir: i0.ɵɵDirectiveDefWithMeta, '[ngFor][ngForOf]', never, {'ngForOf': 'ngForOf'}, {}, never>; } -export declare class NgIf { - ngIf: any; +export declare class NgIf { + ngIf: T; + ngIfElse: TemplateRef> | null; + ngIfThen: TemplateRef> | null; + constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef>); static ngTemplateGuard_ngIf: 'binding'; - static ɵdir: i0.ɵɵDirectiveDefWithMeta, '[ngIf]', never, {'ngIf': 'ngIf'}, {}, never>; + static ngTemplateContextGuard(dir: NgIf, ctx: any): ctx is NgIfContext; + static ɵdir: i0.ɵɵDirectiveDefWithMeta, '[ngIf]', never, {'ngIf': 'ngIf'}, {}, never>; +} + +export declare class NgIfContext { + $implicit: T; + ngIf: T; } export declare class CommonModule { @@ -815,6 +824,33 @@ export declare class AnimationEvent { expect(diags.length).toBe(0); }); + it('should infer the context of NgIf', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true}); + env.write('test.ts', ` + import {CommonModule} from '@angular/common'; + import {Component, NgModule} from '@angular/core'; + @Component({ + selector: 'test', + template: '
{{user.nonExistingProp}}
', + }) + class TestCmp { + getUser(): {name: string} { + return {name: 'frodo'}; + } + } + @NgModule({ + declarations: [TestCmp], + imports: [CommonModule], + }) + class Module {} + `); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText) + .toBe(`Property 'nonExistingProp' does not exist on type '{ name: string; }'.`); + }); + it('should report an error with an unknown local ref target', () => { env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; diff --git a/packages/core/test/render3/common_with_def.ts b/packages/core/test/render3/common_with_def.ts index 0ebf12405a..9a40985fcd 100644 --- a/packages/core/test/render3/common_with_def.ts +++ b/packages/core/test/render3/common_with_def.ts @@ -12,7 +12,7 @@ import {IterableDiffers, NgIterable, TemplateRef, ViewContainerRef} from '@angul import {DirectiveType, ɵɵNgOnChangesFeature, ɵɵdefineDirective, ɵɵdirectiveInject} from '../../src/render3/index'; export const NgForOf: DirectiveType>> = NgForOfDef as any; -export const NgIf: DirectiveType = NgIfDef as any; +export const NgIf: DirectiveType> = NgIfDef as any; export const NgTemplateOutlet: DirectiveType = NgTemplateOutletDef as any; NgForOf.ɵdir = ɵɵdefineDirective({ diff --git a/tools/public_api_guard/common/common.d.ts b/tools/public_api_guard/common/common.d.ts index 2c71a5d748..efa873e405 100644 --- a/tools/public_api_guard/common/common.d.ts +++ b/tools/public_api_guard/common/common.d.ts @@ -235,17 +235,18 @@ export declare class NgForOfContext> { constructor($implicit: T, ngForOf: U, index: number, count: number); } -export declare class NgIf { - ngIf: any; - ngIfElse: TemplateRef | null; - ngIfThen: TemplateRef | null; - constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef); +export declare class NgIf { + ngIf: T; + ngIfElse: TemplateRef> | null; + ngIfThen: TemplateRef> | null; + constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef>); static ngTemplateGuard_ngIf: 'binding'; + static ngTemplateContextGuard(dir: NgIf, ctx: any): ctx is NgIfContext; } -export declare class NgIfContext { - $implicit: any; - ngIf: any; +export declare class NgIfContext { + $implicit: T; + ngIf: T; } export declare class NgLocaleLocalization extends NgLocalization {