Skip to content

Commit 02958c0

Browse files
crisbetomhevery
authored andcommitted
fix(common): reflect input type in NgIf context (angular#33997)
Fixes the content of `NgIf` being typed to any. Fixes angular#31556. PR Close angular#33997
1 parent a6b6d74 commit 02958c0

File tree

4 files changed

+70
-25
lines changed

4 files changed

+70
-25
lines changed

packages/common/src/directives/ng_if.ts

Lines changed: 21 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -149,22 +149,22 @@ import {Directive, EmbeddedViewRef, Input, TemplateRef, ViewContainerRef, ɵstri
149149
* @publicApi
150150
*/
151151
@Directive({selector: '[ngIf]'})
152-
export class NgIf {
153-
private _context: NgIfContext = new NgIfContext();
154-
private _thenTemplateRef: TemplateRef<NgIfContext>|null = null;
155-
private _elseTemplateRef: TemplateRef<NgIfContext>|null = null;
156-
private _thenViewRef: EmbeddedViewRef<NgIfContext>|null = null;
157-
private _elseViewRef: EmbeddedViewRef<NgIfContext>|null = null;
152+
export class NgIf<T> {
153+
private _context: NgIfContext<T> = new NgIfContext<T>();
154+
private _thenTemplateRef: TemplateRef<NgIfContext<T>>|null = null;
155+
private _elseTemplateRef: TemplateRef<NgIfContext<T>>|null = null;
156+
private _thenViewRef: EmbeddedViewRef<NgIfContext<T>>|null = null;
157+
private _elseViewRef: EmbeddedViewRef<NgIfContext<T>>|null = null;
158158

159-
constructor(private _viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext>) {
159+
constructor(private _viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext<T>>) {
160160
this._thenTemplateRef = templateRef;
161161
}
162162

163163
/**
164164
* The Boolean expression to evaluate as the condition for showing a template.
165165
*/
166166
@Input()
167-
set ngIf(condition: any) {
167+
set ngIf(condition: T) {
168168
this._context.$implicit = this._context.ngIf = condition;
169169
this._updateView();
170170
}
@@ -173,7 +173,7 @@ export class NgIf {
173173
* A template to show if the condition expression evaluates to true.
174174
*/
175175
@Input()
176-
set ngIfThen(templateRef: TemplateRef<NgIfContext>|null) {
176+
set ngIfThen(templateRef: TemplateRef<NgIfContext<T>>|null) {
177177
assertTemplate('ngIfThen', templateRef);
178178
this._thenTemplateRef = templateRef;
179179
this._thenViewRef = null; // clear previous view if any.
@@ -184,7 +184,7 @@ export class NgIf {
184184
* A template to show if the condition expression evaluates to false.
185185
*/
186186
@Input()
187-
set ngIfElse(templateRef: TemplateRef<NgIfContext>|null) {
187+
set ngIfElse(templateRef: TemplateRef<NgIfContext<T>>|null) {
188188
assertTemplate('ngIfElse', templateRef);
189189
this._elseTemplateRef = templateRef;
190190
this._elseViewRef = null; // clear previous view if any.
@@ -225,14 +225,22 @@ export class NgIf {
225225
* narrow its type, which allows the strictNullChecks feature of TypeScript to work with `NgIf`.
226226
*/
227227
static ngTemplateGuard_ngIf: 'binding';
228+
229+
/**
230+
* Asserts the correct type of the context for the template that `NgIf` will render.
231+
*
232+
* The presence of this method is a signal to the Ivy template type-check compiler that the
233+
* `NgIf` structural directive renders its template with a specific context type.
234+
*/
235+
static ngTemplateContextGuard<T>(dir: NgIf<T>, ctx: any): ctx is NgIfContext<T> { return true; }
228236
}
229237

230238
/**
231239
* @publicApi
232240
*/
233-
export class NgIfContext {
234-
public $implicit: any = null;
235-
public ngIf: any = null;
241+
export class NgIfContext<T> {
242+
public $implicit: T = null !;
243+
public ngIf: T = null !;
236244
}
237245

238246
function assertTemplate(property: string, templateRef: TemplateRef<any>| null): void {

packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts

Lines changed: 39 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -64,10 +64,19 @@ export declare class NgForOf<T, U extends i0.NgIterable<T>> implements DoCheck {
6464
static ɵdir: i0.ɵɵDirectiveDefWithMeta<NgForOf<any>, '[ngFor][ngForOf]', never, {'ngForOf': 'ngForOf'}, {}, never>;
6565
}
6666
67-
export declare class NgIf {
68-
ngIf: any;
67+
export declare class NgIf<T> {
68+
ngIf: T;
69+
ngIfElse: TemplateRef<NgIfContext<T>> | null;
70+
ngIfThen: TemplateRef<NgIfContext<T>> | null;
71+
constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext<T>>);
6972
static ngTemplateGuard_ngIf: 'binding';
70-
static ɵdir: i0.ɵɵDirectiveDefWithMeta<NgForOf<any>, '[ngIf]', never, {'ngIf': 'ngIf'}, {}, never>;
73+
static ngTemplateContextGuard<T>(dir: NgIf<T>, ctx: any): ctx is NgIfContext<T>;
74+
static ɵdir: i0.ɵɵDirectiveDefWithMeta<NgIf<any>, '[ngIf]', never, {'ngIf': 'ngIf'}, {}, never>;
75+
}
76+
77+
export declare class NgIfContext<T> {
78+
$implicit: T;
79+
ngIf: T;
7180
}
7281
7382
export declare class CommonModule {
@@ -815,6 +824,33 @@ export declare class AnimationEvent {
815824
expect(diags.length).toBe(0);
816825
});
817826

827+
it('should infer the context of NgIf', () => {
828+
env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true});
829+
env.write('test.ts', `
830+
import {CommonModule} from '@angular/common';
831+
import {Component, NgModule} from '@angular/core';
832+
@Component({
833+
selector: 'test',
834+
template: '<div *ngIf="getUser(); let user">{{user.nonExistingProp}}</div>',
835+
})
836+
class TestCmp {
837+
getUser(): {name: string} {
838+
return {name: 'frodo'};
839+
}
840+
}
841+
@NgModule({
842+
declarations: [TestCmp],
843+
imports: [CommonModule],
844+
})
845+
class Module {}
846+
`);
847+
848+
const diags = env.driveDiagnostics();
849+
expect(diags.length).toBe(1);
850+
expect(diags[0].messageText)
851+
.toBe(`Property 'nonExistingProp' does not exist on type '{ name: string; }'.`);
852+
});
853+
818854
it('should report an error with an unknown local ref target', () => {
819855
env.write('test.ts', `
820856
import {Component, NgModule} from '@angular/core';

packages/core/test/render3/common_with_def.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import {IterableDiffers, NgIterable, TemplateRef, ViewContainerRef} from '@angul
1212
import {DirectiveType, ɵɵNgOnChangesFeature, ɵɵdefineDirective, ɵɵdirectiveInject} from '../../src/render3/index';
1313

1414
export const NgForOf: DirectiveType<NgForOfDef<any, NgIterable<any>>> = NgForOfDef as any;
15-
export const NgIf: DirectiveType<NgIfDef> = NgIfDef as any;
15+
export const NgIf: DirectiveType<NgIfDef<any>> = NgIfDef as any;
1616
export const NgTemplateOutlet: DirectiveType<NgTemplateOutletDef> = NgTemplateOutletDef as any;
1717

1818
NgForOf.ɵdir = ɵɵdefineDirective({

tools/public_api_guard/common/common.d.ts

Lines changed: 9 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -235,17 +235,18 @@ export declare class NgForOfContext<T, U extends NgIterable<T>> {
235235
constructor($implicit: T, ngForOf: U, index: number, count: number);
236236
}
237237

238-
export declare class NgIf {
239-
ngIf: any;
240-
ngIfElse: TemplateRef<NgIfContext> | null;
241-
ngIfThen: TemplateRef<NgIfContext> | null;
242-
constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext>);
238+
export declare class NgIf<T> {
239+
ngIf: T;
240+
ngIfElse: TemplateRef<NgIfContext<T>> | null;
241+
ngIfThen: TemplateRef<NgIfContext<T>> | null;
242+
constructor(_viewContainer: ViewContainerRef, templateRef: TemplateRef<NgIfContext<T>>);
243243
static ngTemplateGuard_ngIf: 'binding';
244+
static ngTemplateContextGuard<T>(dir: NgIf<T>, ctx: any): ctx is NgIfContext<T>;
244245
}
245246

246-
export declare class NgIfContext {
247-
$implicit: any;
248-
ngIf: any;
247+
export declare class NgIfContext<T> {
248+
$implicit: T;
249+
ngIf: T;
249250
}
250251

251252
export declare class NgLocaleLocalization extends NgLocalization {

0 commit comments

Comments
 (0)