diff --git a/aio/content/guide/template-typecheck.md b/aio/content/guide/template-typecheck.md index 39b13dd0ec..0a5b35fc9e 100644 --- a/aio/content/guide/template-typecheck.md +++ b/aio/content/guide/template-typecheck.md @@ -120,6 +120,7 @@ In case of a false positive like these, there are a few options: |`strictDomLocalRefTypes`|Whether local references to DOM elements will have the correct type. If disabled `ref` will be of type `any` for ``.| |`strictOutputEventTypes`|Whether `$event` will have the correct type for event bindings to component/directive an `@Output()`, or to animation events. If disabled, it will be `any`.| |`strictDomEventTypes`|Whether `$event` will have the correct type for event bindings to DOM events. If disabled, it will be `any`.| +|`strictContextGenerics`|Whether the type parameters of generic components will be inferred correctly (including any generic bounds). If disabled, any type parameters will be `any`.| If you still have issues after troubleshooting with these flags, you can fall back to full mode by disabling `strictTemplates`. diff --git a/packages/compiler-cli/src/ngtsc/imports/index.ts b/packages/compiler-cli/src/ngtsc/imports/index.ts index 43f63e7c28..6d0c1e9073 100644 --- a/packages/compiler-cli/src/ngtsc/imports/index.ts +++ b/packages/compiler-cli/src/ngtsc/imports/index.ts @@ -9,7 +9,7 @@ export {AliasStrategy, AliasingHost, FileToModuleAliasingHost, PrivateExportAliasingHost} from './src/alias'; export {ImportRewriter, NoopImportRewriter, R3SymbolsImportRewriter, validateAndRewriteCoreSymbol} from './src/core'; export {DefaultImportRecorder, DefaultImportTracker, NOOP_DEFAULT_IMPORT_RECORDER} from './src/default'; -export {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy} from './src/emitter'; +export {AbsoluteModuleStrategy, FileToModuleHost, FileToModuleStrategy, ImportFlags, LocalIdentifierStrategy, LogicalProjectStrategy, ReferenceEmitStrategy, ReferenceEmitter, RelativePathStrategy} from './src/emitter'; export {Reexport} from './src/reexport'; -export {ImportFlags, OwningModule, Reference} from './src/references'; +export {OwningModule, Reference} from './src/references'; export {ModuleResolver} from './src/resolver'; diff --git a/packages/compiler-cli/src/ngtsc/program.ts b/packages/compiler-cli/src/ngtsc/program.ts index d086b7f694..b577f28ad2 100644 --- a/packages/compiler-cli/src/ngtsc/program.ts +++ b/packages/compiler-cli/src/ngtsc/program.ts @@ -504,6 +504,7 @@ export class NgtscProgram implements api.Program { // Pipes are checked in View Engine so there is no strictness flag. checkTypeOfPipes: true, strictSafeNavigationTypes: strictTemplates, + useContextGenericType: strictTemplates, }; } else { typeCheckingConfig = { @@ -521,6 +522,7 @@ export class NgtscProgram implements api.Program { checkTypeOfNonDomReferences: false, checkTypeOfPipes: false, strictSafeNavigationTypes: false, + useContextGenericType: false, }; } @@ -549,6 +551,9 @@ export class NgtscProgram implements api.Program { if (this.options.strictAttributeTypes !== undefined) { typeCheckingConfig.checkTypeOfAttributes = this.options.strictAttributeTypes; } + if (this.options.strictContextGenerics !== undefined) { + typeCheckingConfig.useContextGenericType = this.options.strictContextGenerics; + } // Execute the typeCheck phase of each decorator in the program. const prepSpan = this.perfRecorder.start('typeCheckPrep'); diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts index d5ccb7785e..1032c4b2a5 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/api.ts @@ -211,6 +211,15 @@ export interface TypeCheckingConfig { * This is currently an unsupported feature. */ checkQueries: false; + + /** + * Whether to use any generic types of the context component. + * + * If this is `true`, then if the context component has generic types, those will be mirrored in + * the template type-checking context. If `false`, any generic type parameters of the context + * component will be set to `any` during type-checking. + */ + useContextGenericType: boolean; } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts index 049da807ee..b53b739753 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/type_check_block.ts @@ -57,7 +57,7 @@ export function generateTypeCheckBlock( throw new Error( `Expected TypeReferenceNode when referencing the ctx param for ${ref.debugName}`); } - const paramList = [tcbCtxParam(ref.node, ctxRawType.typeName)]; + const paramList = [tcbCtxParam(ref.node, ctxRawType.typeName, env.config.useContextGenericType)]; const scopeStatements = scope.render(); const innerBody = ts.createBlock([ @@ -73,7 +73,7 @@ export function generateTypeCheckBlock( /* modifiers */ undefined, /* asteriskToken */ undefined, /* name */ name, - /* typeParameters */ ref.node.typeParameters, + /* typeParameters */ env.config.useContextGenericType ? ref.node.typeParameters : undefined, /* parameters */ paramList, /* type */ undefined, /* body */ body); @@ -925,12 +925,18 @@ class Scope { * parameters listed (without their generic bounds). */ function tcbCtxParam( - node: ClassDeclaration, name: ts.EntityName): ts.ParameterDeclaration { + node: ClassDeclaration, name: ts.EntityName, + useGenericType: boolean): ts.ParameterDeclaration { let typeArguments: ts.TypeNode[]|undefined = undefined; // Check if the component is generic, and pass generic type parameters if so. if (node.typeParameters !== undefined) { - typeArguments = - node.typeParameters.map(param => ts.createTypeReferenceNode(param.name, undefined)); + if (useGenericType) { + typeArguments = + node.typeParameters.map(param => ts.createTypeReferenceNode(param.name, undefined)); + } else { + typeArguments = + node.typeParameters.map(() => ts.createKeywordTypeNode(ts.SyntaxKind.AnyKeyword)); + } } const type = ts.createTypeReferenceNode(name, typeArguments); return ts.createParameter( diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts index 6c127651d6..aecbb168cc 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/test_utils.ts @@ -163,6 +163,7 @@ export const ALL_ENABLED_CONFIG: TypeCheckingConfig = { checkTypeOfNonDomReferences: true, checkTypeOfPipes: true, strictSafeNavigationTypes: true, + useContextGenericType: true, }; // Remove 'ref' from TypeCheckableDirectiveMeta and add a 'selector' instead. @@ -217,6 +218,7 @@ export function tcb( checkTypeOfPipes: true, checkTemplateBodies: true, strictSafeNavigationTypes: true, + useContextGenericType: true, }; options = options || { emitSpans: false, diff --git a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts index 50f9aa3e08..2caf51c9ae 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/test/type_check_block_spec.ts @@ -326,6 +326,7 @@ describe('type check blocks', () => { checkTypeOfNonDomReferences: true, checkTypeOfPipes: true, strictSafeNavigationTypes: true, + useContextGenericType: true, }; describe('config.applyTemplateContextGuards', () => { @@ -567,5 +568,20 @@ describe('type check blocks', () => { expect(block).toContain('(((ctx).a) != null ? ((ctx).a)!.b : null as any)'); }); }); + + describe('config.strictContextGenerics', () => { + const TEMPLATE = `Test`; + + it('should use the generic type of the context when enabled', () => { + const block = tcb(TEMPLATE); + expect(block).toContain('function Test_TCB(ctx: Test)'); + }); + + it('should use any for the context generic type when disabled', () => { + const DISABLED_CONFIG: TypeCheckingConfig = {...BASE_CONFIG, useContextGenericType: false}; + const block = tcb(TEMPLATE, undefined, DISABLED_CONFIG); + expect(block).toContain('function Test_TCB(ctx: Test)'); + }); + }); }); }); diff --git a/packages/compiler-cli/src/transformers/api.ts b/packages/compiler-cli/src/transformers/api.ts index 11eed7c2ad..9ffb7ce65c 100644 --- a/packages/compiler-cli/src/transformers/api.ts +++ b/packages/compiler-cli/src/transformers/api.ts @@ -215,6 +215,19 @@ export interface CompilerOptions extends ts.CompilerOptions { */ strictDomEventTypes?: boolean; + /** + * Whether to include the generic type of components when type-checking the template. + * + * If no component has generic type parameters, this setting has no effect. + * + * If a component has generic type parameters and this setting is `true`, those generic parameters + * will be included in the context type for the template. If `false`, any generic parameters will + * be set to `any` in the template context type. + * + * Defaults to `false`, even if "fullTemplateTypeCheck" is set. + */ + strictContextGenerics?: 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 74ce588220..33aa8eae48 100644 --- a/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts +++ b/packages/compiler-cli/test/ngtsc/template_typecheck_spec.ts @@ -972,7 +972,8 @@ export declare class AnimationEvent { }); it('should constrain types using type parameter bounds', () => { - env.tsconfig({fullTemplateTypeCheck: true, strictInputTypes: true}); + env.tsconfig( + {fullTemplateTypeCheck: true, strictInputTypes: true, strictContextGenerics: true}); env.write('test.ts', ` import {CommonModule} from '@angular/common'; import {Component, Input, NgModule} from '@angular/core';