From 0d9be220234718edc9de05b74475a3e7e3e6e391 Mon Sep 17 00:00:00 2001 From: JoostK Date: Sat, 19 Oct 2019 21:27:11 +0200 Subject: [PATCH] feat(ivy): strictness flags for template type checking (#33365) The template type checking abilities of the Ivy compiler are far more advanced than the level of template type checking that was previously done for Angular templates. Up until now, a single compiler option called "fullTemplateTypeCheck" was available to configure the level of template type checking. However, now that more advanced type checking is being done, new errors may surface that were previously not reported, in which case it may not be feasible to fix all new errors at once. Having only a single option to disable a large number of template type checking capabilities does not allow for incrementally addressing newly reported types of errors. As a solution, this commit introduces some new compiler options to be able to enable/disable certain kinds of template type checks on a fine-grained basis. PR Close #33365 --- packages/compiler-cli/src/ngtsc/program.ts | 44 +- .../src/ngtsc/typecheck/src/api.ts | 11 +- packages/compiler-cli/src/transformers/api.ts | 106 +++- .../test/ngtsc/template_typecheck_spec.ts | 458 ++++++++++++++++++ 4 files changed, 604 insertions(+), 15 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index 55f1d4871d99b..d7cd49ea0bc52 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -425,26 +425,29 @@ export class NgtscProgram implements api.Program { // requested. let typeCheckingConfig: TypeCheckingConfig; if (this.options.fullTemplateTypeCheck) { + const strictTemplates = !!this.options.strictTemplates; typeCheckingConfig = { applyTemplateContextGuards: true, checkQueries: false, checkTemplateBodies: true, - checkTypeOfInputBindings: true, - strictNullInputBindings: true, - checkTypeOfAttributes: true, + checkTypeOfInputBindings: strictTemplates, + strictNullInputBindings: strictTemplates, + checkTypeOfAttributes: strictTemplates, // Even in full template type-checking mode, DOM binding checks are not quite ready yet. checkTypeOfDomBindings: false, - checkTypeOfOutputEvents: true, - checkTypeOfAnimationEvents: true, + checkTypeOfOutputEvents: strictTemplates, + checkTypeOfAnimationEvents: strictTemplates, // Checking of DOM events currently has an adverse effect on developer experience, // e.g. for `` enabling this check results in: // - error TS2531: Object is possibly 'null'. // - error TS2339: Property 'value' does not exist on type 'EventTarget'. - checkTypeOfDomEvents: false, - checkTypeOfDomReferences: true, + checkTypeOfDomEvents: strictTemplates, + checkTypeOfDomReferences: strictTemplates, + // Non-DOM references have the correct type in View Engine so there is no strictness flag. checkTypeOfNonDomReferences: true, + // Pipes are checked in View Engine so there is no strictness flag. checkTypeOfPipes: true, - strictSafeNavigationTypes: true, + strictSafeNavigationTypes: strictTemplates, }; } else { typeCheckingConfig = { @@ -465,6 +468,31 @@ export class NgtscProgram implements api.Program { }; } + // Apply explicitly configured strictness flags on top of the default configuration + // based on "fullTemplateTypeCheck". + if (this.options.strictInputTypes !== undefined) { + typeCheckingConfig.checkTypeOfInputBindings = this.options.strictInputTypes; + } + if (this.options.strictNullInputTypes !== undefined) { + typeCheckingConfig.strictNullInputBindings = this.options.strictNullInputTypes; + } + if (this.options.strictOutputEventTypes !== undefined) { + typeCheckingConfig.checkTypeOfOutputEvents = this.options.strictOutputEventTypes; + typeCheckingConfig.checkTypeOfAnimationEvents = this.options.strictOutputEventTypes; + } + if (this.options.strictDomEventTypes !== undefined) { + typeCheckingConfig.checkTypeOfDomEvents = this.options.strictDomEventTypes; + } + if (this.options.strictSafeNavigationTypes !== undefined) { + typeCheckingConfig.strictSafeNavigationTypes = this.options.strictSafeNavigationTypes; + } + if (this.options.strictDomLocalRefTypes !== undefined) { + typeCheckingConfig.checkTypeOfDomReferences = this.options.strictDomLocalRefTypes; + } + if (this.options.strictAttributeTypes !== undefined) { + typeCheckingConfig.checkTypeOfAttributes = this.options.strictAttributeTypes; + } + // Execute the typeCheck phase of each decorator in the program. const prepSpan = this.perfRecorder.start('typeCheckPrep'); const ctx = new TypeCheckContext(typeCheckingConfig, this.refEmitter !, this.typeCheckFilePath); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts index 8ef1f71bc8c40..977943bf02da2 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts @@ -107,10 +107,10 @@ export interface TypeCheckingConfig { * Whether to check text attributes that happen to be consumed by a directive or component. * * For example, in a template containing `` the `disabled` attribute ends - * up being consumed as an input with type `boolean` by the `matInput` directive. At runtime the - * input will be set to the attribute's string value, which is the empty string for attributes - * without a value, so with this flag set to `true` an error would be reported. If set to `false`, - * text attributes will never report an error. + * up being consumed as an input with type `boolean` by the `matInput` directive. At runtime, the + * input will be set to the attribute's string value, which is an empty string for attributes + * without a value, so with this flag set to `true`, an error would be reported. If set to + * `false`, text attributes will never report an error. * * Note that if `checkTypeOfInputBindings` is set to `false`, this flag has no effect. */ @@ -129,7 +129,8 @@ export interface TypeCheckingConfig { checkTypeOfDomBindings: boolean; /** - * Whether to infer the type of the `$event` variable in event bindings for directive outputs. + * Whether to infer the type of the `$event` variable in event bindings for directive outputs or + * animation events. * * If this is `true`, the type of `$event` will be inferred based on the generic type of * `EventEmitter`/`Subject` of the output. If set to `false`, the `$event` variable will be of diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 2fb4466ffeb26..86d88cadc4a30 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -108,10 +108,112 @@ export interface CompilerOptions extends ts.CompilerOptions { // Default is true. generateCodeForLibraries?: boolean; - // Whether to enable all type checks for templates. - // This will be true be default in Angular 6. + /** + * Whether to type check the entire template. + * + * This flag currently controls a couple aspects of template type-checking, including + * whether embedded views are checked. + * + * For maximum type-checking, set this to `true`, and set `strictTemplates` to `true`. + */ fullTemplateTypeCheck?: boolean; + /** + * If `true`, implies all template strictness flags below (unless individually disabled). + * + * Has no effect unless `fullTemplateTypeCheck` is also enabled. + * + * Defaults to `false`, even if "fullTemplateTypeCheck" is set. + */ + strictTemplates?: boolean; + + + /** + * Whether to check the type of a binding to a directive/component input against the type of the + * field on the directive/component. + * + * For example, if this is `false` then the expression `[input]="expr"` will have `expr` type- + * checked, but not the assignment of the resulting type to the `input` property of whichever + * directive or component is receiving the binding. If set to `true`, both sides of the assignment + * are checked. + * + * Defaults to `false`, even if "fullTemplateTypeCheck" is set. + */ + strictInputTypes?: boolean; + + /** + * Whether to use strict null types for input bindings for directives. + * + * If this is `true`, applications that are compiled with TypeScript's `strictNullChecks` enabled + * will produce type errors for bindings which can evaluate to `undefined` or `null` where the + * inputs's type does not include `undefined` or `null` in its type. If set to `false`, all + * binding expressions are wrapped in a non-null assertion operator to effectively disable strict + * null checks. + * + * Defaults to `false`, even if "fullTemplateTypeCheck" is set. Note that if `strictInputTypes` is + * not set, or set to `false`, this flag has no effect. + */ + strictNullInputTypes?: boolean; + + /** + * Whether to check text attributes that happen to be consumed by a directive or component. + * + * For example, in a template containing `` the `disabled` attribute ends + * up being consumed as an input with type `boolean` by the `matInput` directive. At runtime, the + * input will be set to the attribute's string value, which is an empty string for attributes + * without a value, so with this flag set to `true`, an error would be reported. If set to + * `false`, text attributes will never report an error. + * + * Defaults to `false`, even if "fullTemplateTypeCheck" is set. Note that if `strictInputTypes` is + * not set, or set to `false`, this flag has no effect. + */ + strictAttributeTypes?: boolean; + + /** + * Whether to use a strict type for null-safe navigation operations. + * + * If this is `false`, then the return type of `a?.b` or `a?()` will be `any`. If set to `true`, + * then the return type of `a?.b` for example will be the same as the type of the ternary + * expression `a != null ? a.b : a`. + * + * Defaults to `false`, even if "fullTemplateTypeCheck" is set. + */ + strictSafeNavigationTypes?: boolean; + + /** + * Whether to infer the type of local references. + * + * If this is `true`, the type of a `#ref` variable on a DOM node in the template will be + * determined by the type of `document.createElement` for the given DOM node. If set to `false`, + * the type of `ref` for DOM nodes will be `any`. + * + * Defaults to `false`, even if "fullTemplateTypeCheck" is set. + */ + strictDomLocalRefTypes?: boolean; + + /** + * Whether to infer the type of the `$event` variable in event bindings for directive outputs or + * animation events. + * + * If this is `true`, the type of `$event` will be inferred based on the generic type of + * `EventEmitter`/`Subject` of the output. If set to `false`, the `$event` variable will be of + * type `any`. + * + * Defaults to `false`, even if "fullTemplateTypeCheck" is set. + */ + strictOutputEventTypes?: boolean; + + /** + * Whether to infer the type of the `$event` variable in event bindings to DOM events. + * + * If this is `true`, the type of `$event` will be inferred based on TypeScript's + * `HTMLElementEventMap`, with a fallback to the native `Event` type. If set to `false`, the + * `$event` variable will be of type `any`. + * + * Defaults to `false`, even if "fullTemplateTypeCheck" is set. + */ + strictDomEventTypes?: boolean; + // Whether to use the CompilerHost's fileNameToModuleName utility (if available) to generate // import module specifiers. This is false by default, and exists to support running ngtsc // within Google. This option is internal and is used by the ng_module.bzl rule to switch diff --git a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts index 6b0293592b3ab..9522cf8a82e2b 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -58,6 +58,11 @@ export declare class NgIf { export declare class CommonModule { static ɵmod: i0.ɵɵNgModuleDefWithMeta; } +`); + env.write('node_modules/@angular/animations/index.d.ts', ` +export declare class AnimationEvent { + element: any; +} `); }); @@ -81,6 +86,8 @@ export declare class CommonModule { }); it('should check regular attributes that are directive inputs', () => { + env.tsconfig( + {fullTemplateTypeCheck: true, strictInputTypes: true, strictAttributeTypes: true}); env.write('test.ts', ` import {Component, Directive, NgModule, Input} from '@angular/core'; @@ -107,6 +114,7 @@ export declare class CommonModule { }); it('should check event bindings', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictOutputEventTypes: true}); env.write('test.ts', ` import {Component, Directive, EventEmitter, NgModule, Output} from '@angular/core'; @@ -142,6 +150,449 @@ export declare class CommonModule { expect(diags[2].messageText).toEqual(`Property 'focused' does not exist on type 'TestCmp'.`); }); + describe('strictInputTypes', () => { + beforeEach(() => { + env.write('test.ts', ` + import {Component, Directive, NgModule, Input} from '@angular/core'; + + @Component({ + selector: 'test', + template: '
', + }) + class TestCmp {} + + @Directive({selector: '[dir]'}) + class TestDir { + @Input() foo: string; + } + + @NgModule({ + declarations: [TestCmp, TestDir], + }) + class Module {} + `); + }); + + it('should check expressions and their type when enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect(diags[0].messageText).toEqual(`Type 'number' is not assignable to type 'string'.`); + expect(diags[1].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + }); + + it('should check expressions and their type when overall strictness is enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect(diags[0].messageText).toEqual(`Type 'number' is not assignable to type 'string'.`); + expect(diags[1].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + }); + + it('should check expressions but not their type when not enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + }); + }); + + describe('strictNullInputTypes', () => { + beforeEach(() => { + env.write('test.ts', ` + import {Component, Directive, NgModule, Input} from '@angular/core'; + + @Component({ + selector: 'test', + template: '
', + }) + class TestCmp { + nullable: string | null | undefined; + } + + @Directive({selector: '[dir]'}) + class TestDir { + @Input() foo: string; + } + + @NgModule({ + declarations: [TestCmp, TestDir], + }) + class Module {} + `); + }); + + it('should check expressions and their nullability when enabled', () => { + env.tsconfig( + {fullTemplateTypeCheck: true, strictInputTypes: true, strictNullInputTypes: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect((diags[0].messageText as ts.DiagnosticMessageChain).messageText) + .toEqual(`Type 'string | null | undefined' is not assignable to type 'string'.`); + expect(diags[1].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + }); + + it('should check expressions and their nullability when overall strictness is enabled', + () => { + env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect((diags[0].messageText as ts.DiagnosticMessageChain).messageText) + .toEqual(`Type 'string | null | undefined' is not assignable to type 'string'.`); + expect(diags[1].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + }); + + it('should check expressions but not their nullability when not enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + }); + }); + + describe('strictSafeNavigationTypes', () => { + beforeEach(() => { + env.write('test.ts', ` + import {Component, Directive, NgModule, Input} from '@angular/core'; + + @Component({ + selector: 'test', + template: '
', + }) + class TestCmp { + user?: {name: string}; + } + + @Directive({selector: '[dir]'}) + class TestDir { + @Input() foo: string; + } + + @NgModule({ + declarations: [TestCmp, TestDir], + }) + class Module {} + `); + }); + + it('should infer result type for safe navigation expressions when enabled', () => { + env.tsconfig({ + fullTemplateTypeCheck: true, + strictInputTypes: true, + strictNullInputTypes: true, + strictSafeNavigationTypes: true + }); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect((diags[0].messageText as ts.DiagnosticMessageChain).messageText) + .toEqual(`Type 'string | undefined' is not assignable to type 'string'.`); + expect(diags[1].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + }); + + it('should infer result type for safe navigation expressions when overall strictness is enabled', + () => { + env.tsconfig({ + fullTemplateTypeCheck: true, + strictTemplates: true, + }); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect((diags[0].messageText as ts.DiagnosticMessageChain).messageText) + .toEqual(`Type 'string | undefined' is not assignable to type 'string'.`); + expect(diags[1].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + }); + + it('should not infer result type for safe navigation expressions when not enabled', () => { + env.tsconfig({ + fullTemplateTypeCheck: true, + strictInputTypes: true, + }); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + }); + }); + + describe('strictOutputEventTypes', () => { + beforeEach(() => { + env.write('test.ts', ` + import {Component, Directive, EventEmitter, NgModule, Output} from '@angular/core'; + + @Component({ + selector: 'test', + template: '
', + }) + class TestCmp { + update(data: string) {} + } + + @Directive({selector: '[dir]'}) + class TestDir { + @Output() update = new EventEmitter(); + } + + @NgModule({ + declarations: [TestCmp, TestDir], + }) + class Module {} + `); + }); + + it('should expressions and infer type of $event when enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictOutputEventTypes: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect(diags[0].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + expect(diags[1].messageText) + .toEqual(`Argument of type 'number' is not assignable to parameter of type 'string'.`); + }); + + it('should expressions and infer type of $event when overall strictness is enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect(diags[0].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + expect(diags[1].messageText) + .toEqual(`Argument of type 'number' is not assignable to parameter of type 'string'.`); + }); + + it('should check expressions but not infer type of $event when not enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + }); + }); + + describe('strictOutputEventTypes and animation event bindings', () => { + beforeEach(() => { + env.write('test.ts', ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'test', + template: '
', + }) + class TestCmp { + update(data: string) {} + } + + @NgModule({ + declarations: [TestCmp], + }) + class Module {} + `); + }); + + it('should check expressions and let $event be of type AnimationEvent when enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictOutputEventTypes: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect(diags[0].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + expect(diags[1].messageText) + .toEqual( + `Argument of type 'AnimationEvent' is not assignable to parameter of type 'string'.`); + }); + + it('should check expressions and let $event be of type AnimationEvent when overall strictness is enabled', + () => { + env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect(diags[0].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + expect(diags[1].messageText) + .toEqual( + `Argument of type 'AnimationEvent' is not assignable to parameter of type 'string'.`); + }); + + it('should check expressions and let $event be of type any when not enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + }); + }); + + describe('strictDomLocalRefTypes', () => { + beforeEach(() => { + env.write('test.ts', ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'test', + template: '{{ref.does_not_exist}}', + }) + class TestCmp {} + + @NgModule({ + declarations: [TestCmp], + }) + class Module {} + `); + }); + + it('should infer the type of DOM references when enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictDomLocalRefTypes: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText) + .toEqual(`Property 'does_not_exist' does not exist on type 'HTMLInputElement'.`); + }); + + it('should infer the type of DOM references when overall strictness is enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText) + .toEqual(`Property 'does_not_exist' does not exist on type 'HTMLInputElement'.`); + }); + + it('should let the type of DOM references be any when not enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + }); + }); + + describe('strictAttributeTypes', () => { + beforeEach(() => { + env.write('test.ts', ` + import {Component, Directive, NgModule, Input} from '@angular/core'; + + @Component({ + selector: 'test', + template: '', + }) + class TestCmp {} + + @Directive({selector: '[dir]'}) + class TestDir { + @Input() disabled: boolean; + @Input() cols: number; + } + + @NgModule({ + declarations: [TestCmp, TestDir], + }) + class Module {} + `); + }); + + it('should produce an error for text attributes when enabled', () => { + env.tsconfig( + {fullTemplateTypeCheck: true, strictInputTypes: true, strictAttributeTypes: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect(diags[0].messageText).toEqual(`Type 'string' is not assignable to type 'boolean'.`); + expect(diags[1].messageText).toEqual(`Type 'string' is not assignable to type 'number'.`); + }); + + it('should produce an error for text attributes when overall strictness is enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect(diags[0].messageText).toEqual(`Type 'string' is not assignable to type 'boolean'.`); + expect(diags[1].messageText).toEqual(`Type 'string' is not assignable to type 'number'.`); + }); + + it('should not produce an error for text attributes when not enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + }); + }); + + describe('strictDomEventTypes', () => { + beforeEach(() => { + env.write('test.ts', ` + import {Component, NgModule} from '@angular/core'; + + @Component({ + selector: 'test', + template: '
', + }) + class TestCmp { + update(data: string) {} + } + + @NgModule({ + declarations: [TestCmp], + }) + class Module {} + `); + }); + + it('should check expressions and infer type of $event when enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictDomEventTypes: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect(diags[0].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + expect(diags[1].messageText) + .toEqual( + `Argument of type 'FocusEvent' is not assignable to parameter of type 'string'.`); + }); + + it('should check expressions and infer type of $event when overall strictness is enabled', + () => { + env.tsconfig({fullTemplateTypeCheck: true, strictTemplates: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(2); + expect(diags[0].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + expect(diags[1].messageText) + .toEqual( + `Argument of type 'FocusEvent' is not assignable to parameter of type 'string'.`); + }); + + it('should check expressions but not infer type of $event when not enabled', () => { + env.tsconfig({fullTemplateTypeCheck: true}); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(1); + expect(diags[0].messageText) + .toEqual(`Property 'invalid' does not exist on type 'TestCmp'.`); + }); + }); + it('should check basic usage of NgIf', () => { env.write('test.ts', ` import {CommonModule} from '@angular/common'; @@ -212,6 +663,7 @@ export declare class CommonModule { }); it('should report an error inside the NgFor template', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true}); env.write('test.ts', ` import {CommonModule} from '@angular/common'; import {Component, NgModule} from '@angular/core'; @@ -315,6 +767,7 @@ export declare class CommonModule { }); it('should constrain types using type parameter bounds', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true}); env.write('test.ts', ` import {CommonModule} from '@angular/common'; import {Component, Input, NgModule} from '@angular/core'; @@ -367,6 +820,7 @@ export declare class CommonModule { }); it('should properly type-check inherited directives', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true}); env.write('test.ts', ` import {Component, Directive, Input, NgModule} from '@angular/core'; @@ -412,6 +866,8 @@ export declare class CommonModule { }); it('should properly type-check inherited directives from external libraries', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true}); + env.write('node_modules/external/index.d.ts', ` import * as i0 from '@angular/core'; @@ -511,6 +967,8 @@ export declare class CommonModule { it('should give an error if the binding expression type is not accepted by the coercion function', () => { + env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true}); + env.write('test.ts', ` import {Component, NgModule} from '@angular/core'; import {MatInputModule} from '@angular/material';