From 2c6781071f52d6378a002fba6611bb283fbb2fde Mon Sep 17 00:00:00 2001 From: Kristiyan Kostadinov Date: Tue, 7 Apr 2026 09:09:04 +0200 Subject: [PATCH] fix(compiler-cli): error for type parameter declarations Fixes an error that was heppning when a generic param has type parameters of its own. There were a few different issues going on: 1. In #67707 I had changed a bit how we pass the `genericContextBehavior` which ended up ignoring the `useContextGenericType` option from the environment. 2. All directives depend on themselves, but we were overridding the `genericContextBehavior` for the directive being processed. 3. The type translator wasn't handling type parameter declarations. Technically we shouldn't be able to hit a code path that has a type parameter, however it's also easy enough to handle so we might as well. Relates to #67704. (cherry picked from commit ab061a7610bfcc5aad15fdc2d812085ae3e8d9b1) --- .../ngtsc/translator/src/type_translator.ts | 6 ++- .../src/ngtsc/typecheck/src/tcb_adapter.ts | 18 ++++---- .../compiler-cli/test/ngtsc/ngtsc_spec.ts | 43 ++++++++++++++++--- 3 files changed, 50 insertions(+), 17 deletions(-) diff --git a/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts b/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts index 52eb0546392..c1f5e4e3f92 100644 --- a/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts +++ b/packages/compiler-cli/src/ngtsc/translator/src/type_translator.ts @@ -251,14 +251,16 @@ class TypeTranslatorVisitor implements o.ExpressionVisitor, o.TypeVisitor { visitWrappedNodeExpr(ast: o.WrappedNodeExpr, context: Context): ts.TypeNode { const node: ts.Node = ast.node; if (ts.isEntityName(node)) { - return ts.factory.createTypeReferenceNode(node, /* typeArguments */ undefined); + return ts.factory.createTypeReferenceNode(node); } else if (ts.isTypeNode(node)) { return node; } else if (ts.isLiteralExpression(node)) { return ts.factory.createLiteralTypeNode(node); + } else if (ts.isTypeParameterDeclaration(node)) { + return ts.factory.createTypeReferenceNode(node.name); } else { throw new Error( - `Unsupported WrappedNodeExpr in TypeTranslatorVisitor: ${ts.SyntaxKind[node.kind]}`, + `Unsupported WrappedNodeExpr in TypeTranslatorVisitor: ${ts.SyntaxKind[node.kind]} in ${node.getSourceFile()?.fileName}`, ); } } diff --git a/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_adapter.ts b/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_adapter.ts index 3be223c35d6..d62de049511 100644 --- a/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_adapter.ts +++ b/packages/compiler-cli/src/ngtsc/typecheck/src/tcb_adapter.ts @@ -121,7 +121,11 @@ export function adaptTypeCheckBlockMetadata( ...adaptGenerics( dir.ref.node as ClassDeclaration, env, - TcbGenericContextBehavior.UseEmitter, + // The directive that we're processing is its own dependency + // so we should the same generic context behavior. + extractRef(dir.ref).key === extractRef(ref).key + ? genericContextBehavior + : TcbGenericContextBehavior.UseEmitter, ), }; @@ -201,13 +205,7 @@ export function adaptTypeCheckBlockMetadata( }, component: { ref: extractRef(ref as Reference), - ...adaptGenerics( - ref.node, - env, - env.config.useContextGenericType - ? genericContextBehavior - : TcbGenericContextBehavior.FallbackToAny, - ), + ...adaptGenerics(ref.node, env, genericContextBehavior), }, }; } @@ -224,6 +222,10 @@ function adaptGenerics( let typeArguments: string[] | null; if (node.typeParameters !== undefined && node.typeParameters.length > 0) { + if (!env.config.useContextGenericType) { + genericContextBehavior = TcbGenericContextBehavior.FallbackToAny; + } + switch (genericContextBehavior) { case TcbGenericContextBehavior.UseEmitter: const emitter = new TypeParameterEmitter(node.typeParameters, env.reflector); diff --git a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts index 975b9a2bc7b..52b769ccede 100644 --- a/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts +++ b/packages/compiler-cli/test/ngtsc/ngtsc_spec.ts @@ -10788,10 +10788,12 @@ runInEachFileSystem((os: string) => { expect(codes).toEqual([ngErrorCode(ErrorCode.NGMODULE_BOOTSTRAP_IS_STANDALONE)]); }); - it('should compile a component with a complex generic', () => { - env.write( - 'test.ts', - ` + [true, false].forEach((strictTemplates) => { + it(`[strictTemplates: ${strictTemplates}] should compile a component with a complex generic`, () => { + env.tsconfig({strictTemplates}); + env.write( + 'test.ts', + ` import {Component} from '@angular/core'; @Component({ @@ -10803,10 +10805,37 @@ runInEachFileSystem((os: string) => { TOptions extends { [K in keyof T]?: T[K] } = object > {} `, - ); + ); - const diags = env.driveDiagnostics(); - expect(diags.length).toBe(0); + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + }); + + // See #67704. + it(`[strictTemplates: ${strictTemplates}] should compile a directive with a generic that has type parameters`, () => { + env.tsconfig({strictTemplates}); + env.write( + 'test.ts', + ` + import {Directive} from '@angular/core'; + + type Foo = {prop: T}; + + @Directive({ + host: { + '[class.some-class]': 'foo || bar' // Only necessary to enable type checking. + }, + }) + export class TestDir ? V : never> { + foo?: T; + bar?: U; + } + `, + ); + + const diags = env.driveDiagnostics(); + expect(diags.length).toBe(0); + }); }); describe('InjectorDef emit optimizations for standalone', () => {