From eb6975acaf4b0941be718f809d76352c6455bfda Mon Sep 17 00:00:00 2001 From: Alex Rickabaugh Date: Fri, 1 Nov 2019 12:47:14 -0700 Subject: [PATCH] fix(ivy): don't infer template context types when in full mode (#33537) The Ivy template type-checker is capable of inferring the type of a structural directive (such as NgForOf). Previously, this was done with fullTemplateTypeCheck: true, even if strictTemplates was false. View Engine previously did not do this inference, and so this causes breakages if the type of the template context is not what the user expected. In particular, consider the template: ```html
{{user.index}} out of {{all.length}}
``` As long as `users` is an array, this seems reasonable, because it appears that `all` is an alias for the `users` array. However, this is misleading. In reality, `NgForOf` is rendered with a template context that contains both a `$implicit` value (for the loop variable `user`) as well as a `ngForOf` value, which is the actual value assigned to `all`. The type of `NgForOf`'s template context is `NgForContext`, which declares `ngForOf`'s type to be `NgIterable`, which does not have a `length` property (due to its incorporation of the `Iterable` type). This commit stops the template type-checker from inferring template context types unless strictTemplates is set (and strictInputTypes is not disabled). Fixes #33527. PR Close #33537 --- packages/compiler-cli/src/ngtsc/program.ts | 3 +- .../test/ngtsc/template_typecheck_spec.ts | 72 +++++++++++++------ 2 files changed, 52 insertions(+), 23 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index aa9c343a77..1ec33392c0 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -423,7 +423,7 @@ export class NgtscProgram implements api.Program { if (this.options.fullTemplateTypeCheck) { const strictTemplates = !!this.options.strictTemplates; typeCheckingConfig = { - applyTemplateContextGuards: true, + applyTemplateContextGuards: strictTemplates, checkQueries: false, checkTemplateBodies: true, checkTypeOfInputBindings: strictTemplates, @@ -468,6 +468,7 @@ export class NgtscProgram implements api.Program { // based on "fullTemplateTypeCheck". if (this.options.strictInputTypes !== undefined) { typeCheckingConfig.checkTypeOfInputBindings = this.options.strictInputTypes; + typeCheckingConfig.applyTemplateContextGuards = this.options.strictInputTypes; } if (this.options.strictNullInputTypes !== undefined) { typeCheckingConfig.strictNullInputBindings = this.options.strictNullInputTypes; diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index d583fc1e0d..1f41873e0b 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -43,6 +43,16 @@ export declare class IndexPipe { static ɵpipe: i0.ɵPipeDefWithMeta; } +export declare class SlicePipe { + transform(value: ReadonlyArray, start: number, end?: number): Array; + transform(value: string, start: number, end?: number): string; + transform(value: null, start: number, end?: number): null; + transform(value: undefined, start: number, end?: number): undefined; + transform(value: any, start: number, end?: number): any; + + static ɵpipe: i0.ɵPipeDefWithMeta; +} + export declare class NgForOf { ngForOf: i0.NgIterable; static ngTemplateContextGuard(dir: NgForOf, ctx: any): ctx is NgForOfContext; @@ -56,7 +66,7 @@ export declare class NgIf { } export declare class CommonModule { - static ɵmod: i0.ɵɵNgModuleDefWithMeta; + static ɵmod: i0.ɵɵNgModuleDefWithMeta; } `); env.write('node_modules/@angular/animations/index.d.ts', ` @@ -862,30 +872,48 @@ export declare class AnimationEvent { expect(diags[0].length).toBe(19); }); - it('should property type-check a microsyntax variable with the same name as the expression', - () => { - env.write('test.ts', ` - import {CommonModule} from '@angular/common'; - import {Component, Input, NgModule} from '@angular/core'; + describe('microsyntax variables', () => { + beforeEach(() => { + // Use the same template for both tests + env.write('test.ts', ` + import {CommonModule} from '@angular/common'; + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'test', + template: \`
+ {{foo.name}} of {{foos.length}} +
+ \`, + }) + export class TestCmp { + foos: {name: string}[]; + } + + @NgModule({ + declarations: [TestCmp], + imports: [CommonModule], + }) + export class Module {} + `); + }); - @Component({ - selector: 'test', - template: '
{{foo}}
', - }) - export class TestCmp { - foo: any; - } + it('should be treated as \'any\' without strictTemplates', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: false}); - @NgModule({ - declarations: [TestCmp], - imports: [CommonModule], - }) - export class Module {} - `); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + }); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(0); - }); + it('should be correctly inferred under strictTemplates', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect((diags[0].messageText as ts.DiagnosticMessageChain).messageText) + .toBe(`Property 'length' does not exist on type 'NgIterable<{ name: string; }>'.`); + }); + }); it('should properly type-check inherited directives', () => { env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true});