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<S>`) 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.
This commit is contained in:
Chris Tate
2026-10-22 03:10:12 -05:00
parent d68b73e3b4
commit b2b216c3df
2 changed files with 57 additions and 9 deletions
@@ -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<S>`).
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;
@@ -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<S>(value: S): Struct<S> {
return class {
static decode(_input: unknown): S {
return value;
}
} as unknown as Struct<S>;
}
class Anonymous extends anonymous({ tag: "anon" }) {}
console.log(Anonymous.decode(null).tag);
type Holder<T> = (new (held: T) => { held: T }) & { readonly seed: () => T };
function holder<T>(initial: T): Holder<T> {
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());