From b2b216c3df6184fd964ad3aee3ee69c07e7ecd07 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 22 Oct 2026 03:10:12 -0500 Subject: [PATCH] Extend a generic factory's anonymous local class returned through a constructor type A generic factory declared to return a constructor type now also extends when it returns an anonymous class expression (`return class { ... } as Struct`) or a `const` bound to one (`const Hold = class { ... }; return Hold`). The result's class value is the class expression's own constructor type; before, it was read through the class's name, so an anonymous class refused with SC1090 and SC2008. --- .../frontend/lowering/generic-functions.ts | 34 ++++++++++++++----- .../corpus/6912-extend-factory-local-class.ts | 32 +++++++++++++++++ 2 files changed, 57 insertions(+), 9 deletions(-) diff --git a/packages/compiler/src/frontend/lowering/generic-functions.ts b/packages/compiler/src/frontend/lowering/generic-functions.ts index 0f549d889..e843b6909 100644 --- a/packages/compiler/src/frontend/lowering/generic-functions.ts +++ b/packages/compiler/src/frontend/lowering/generic-functions.ts @@ -496,10 +496,18 @@ function localClassResultInstance( const viaConstructorType = retTs.getSymbol()?.valueDeclaration !== resultClass; const returnType = withInstanceScope(lowerer, info, inst, () => { if (!viaConstructorType) return lowerer.mapTypeOf(retTs); - const classSymbol = resultClass.name - ? lowerer.checker.getSymbolAtLocation(resultClass.name) - : undefined; - return classSymbol ? lowerer.mapTypeOf(lowerer.checker.getTypeOfSymbol(classSymbol)) : null; + // The local class's own constructor type: a class expression's type is + // its constructor type, named or not (`return class { ... } as Struct`). + const classSymbol = + !ts.isClassExpression(resultClass) && resultClass.name + ? lowerer.checker.getSymbolAtLocation(resultClass.name) + : undefined; + const classType = ts.isClassExpression(resultClass) + ? lowerer.checker.getTypeAtLocation(resultClass) + : classSymbol + ? lowerer.checker.getTypeOfSymbol(classSymbol) + : undefined; + return classType ? lowerer.mapTypeOf(classType) : null; }); if (!returnType) { fenceGenericSignatureResult(lowerer, expr, retTs); @@ -646,11 +654,19 @@ function returnedLocalClass(lowerer: Lowerer, info: GenericFnInfo): ts.ClassLike if (ts.isClassExpression(expr)) return declaredInside(expr, info.decl) ? expr : null; if (!ts.isIdentifier(expr)) return null; const declaration = lowerer.checker.getSymbolAtLocation(expr)?.valueDeclaration; - return declaration && - ts.isClassDeclaration(declaration) && - declaredInside(declaration, info.decl) - ? declaration - : null; + if (!declaration || !declaredInside(declaration, info.decl)) return null; + if (ts.isClassDeclaration(declaration)) return declaration; + // A `const` bound to a class expression (`const C = class { ... }; return C`). + if ( + ts.isVariableDeclaration(declaration) && + ts.getCombinedNodeFlags(declaration) & ts.NodeFlags.Const && + declaration.initializer + ) { + let init: ts.Expression = declaration.initializer; + while (ts.isParenthesizedExpression(init)) init = init.expression; + if (ts.isClassExpression(init)) return init; + } + return null; }; if (!ts.isBlock(body)) return classOf(body as ts.Expression); let found: ts.ClassLikeDeclaration | null = null; diff --git a/tests/corpus/6912-extend-factory-local-class.ts b/tests/corpus/6912-extend-factory-local-class.ts index 8848280e2..e33a59c77 100644 --- a/tests/corpus/6912-extend-factory-local-class.ts +++ b/tests/corpus/6912-extend-factory-local-class.ts @@ -65,3 +65,35 @@ function twice(n: number): number { return K.double() + K.quad(); } console.log(twice(3), twice(5)); + +// The factory returns an anonymous class expression, directly or through a +// `const` binding. +function anonymous(value: S): Struct { + return class { + static decode(_input: unknown): S { + return value; + } + } as unknown as Struct; +} +class Anonymous extends anonymous({ tag: "anon" }) {} +console.log(Anonymous.decode(null).tag); + +type Holder = (new (held: T) => { held: T }) & { readonly seed: () => T }; +function holder(initial: T): Holder { + const Hold = class { + held: T; + constructor(held: T) { + this.held = held; + } + static seed(): T { + return initial; + } + }; + return Hold; +} +class Boxed extends holder(7) { + twice(): number { + return this.held * 2; + } +} +console.log(new Boxed(4).twice(), Boxed.seed());