From e3cd3797516403bbcb530806e2167595649965b3 Mon Sep 17 00:00:00 2001
From: Thiago Barbosa
Date: Wed, 22 Jul 2026 19:21:25 +0200
Subject: [PATCH] fix: Incorrect type resolution with nested constructors
Resolves microsoft/TypeScript#63559
https://github.com/microsoft/TypeScript/issues/63559
Generated by issue-coder. Review before pushing.
---
src/compiler/checker.ts | 5 ++
...ndsConstructorReturningConstructor.symbols | 41 ++++++++++++
...tendsConstructorReturningConstructor.types | 64 +++++++++++++++++++
...sExtendsConstructorReturningConstructor.ts | 20 ++++++
4 files changed, 130 insertions(+)
create mode 100644 tests/baselines/reference/classExtendsConstructorReturningConstructor.symbols
create mode 100644 tests/baselines/reference/classExtendsConstructorReturningConstructor.types
create mode 100644 tests/cases/compiler/classExtendsConstructorReturningConstructor.ts
diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts
index 0567712f11da3..69fbc29e0c720 100644
--- a/src/compiler/checker.ts
+++ b/src/compiler/checker.ts
@@ -13325,6 +13325,11 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
const originalBaseType = baseConstructorType.symbol ? getDeclaredTypeOfSymbol(baseConstructorType.symbol) : undefined;
if (
baseConstructorType.symbol && baseConstructorType.symbol.flags & SymbolFlags.Class &&
+ // Guard against extending a value whose type is a class instance type (rather than the class
+ // constructor itself). In that case `baseConstructorType` is the instance type, which shares the
+ // class symbol but whose construct signatures do not return the instance type, so we must resolve
+ // the base type from those construct signatures below instead of assuming the instance type.
+ baseConstructorType !== originalBaseType &&
areAllOuterTypeParametersApplied(originalBaseType!)
) {
// When base constructor type is a class with no captured type arguments we know that the constructors all have the same type parameters as the
diff --git a/tests/baselines/reference/classExtendsConstructorReturningConstructor.symbols b/tests/baselines/reference/classExtendsConstructorReturningConstructor.symbols
new file mode 100644
index 0000000000000..57af0afa7f49e
--- /dev/null
+++ b/tests/baselines/reference/classExtendsConstructorReturningConstructor.symbols
@@ -0,0 +1,41 @@
+//// [tests/cases/compiler/classExtendsConstructorReturningConstructor.ts] ////
+
+=== classExtendsConstructorReturningConstructor.ts ===
+// https://github.com/microsoft/TypeScript/issues/63559
+// Extending a value whose type is a class instance type carrying a construct
+// signature should resolve the base type from that construct signature rather
+// than repeating the outer instance type.
+
+export declare const Base: {
+>Base : Symbol(Base, Decl(classExtendsConstructorReturningConstructor.ts, 5, 20))
+
+ new (): {
+ kind: "A";
+>kind : Symbol(kind, Decl(classExtendsConstructorReturningConstructor.ts, 6, 13))
+
+ new (): { kind: "B" };
+>kind : Symbol(kind, Decl(classExtendsConstructorReturningConstructor.ts, 8, 17))
+
+ };
+};
+
+const A = new class extends Base {}();
+>A : Symbol(A, Decl(classExtendsConstructorReturningConstructor.ts, 12, 5))
+>Base : Symbol(Base, Decl(classExtendsConstructorReturningConstructor.ts, 5, 20))
+
+const B = new class extends A {}();
+>B : Symbol(B, Decl(classExtendsConstructorReturningConstructor.ts, 13, 5))
+>A : Symbol(A, Decl(classExtendsConstructorReturningConstructor.ts, 12, 5))
+
+const a: "A" = A.kind;
+>a : Symbol(a, Decl(classExtendsConstructorReturningConstructor.ts, 15, 5))
+>A.kind : Symbol(kind, Decl(classExtendsConstructorReturningConstructor.ts, 6, 13))
+>A : Symbol(A, Decl(classExtendsConstructorReturningConstructor.ts, 12, 5))
+>kind : Symbol(kind, Decl(classExtendsConstructorReturningConstructor.ts, 6, 13))
+
+const b: "B" = B.kind;
+>b : Symbol(b, Decl(classExtendsConstructorReturningConstructor.ts, 16, 5))
+>B.kind : Symbol(kind, Decl(classExtendsConstructorReturningConstructor.ts, 8, 17))
+>B : Symbol(B, Decl(classExtendsConstructorReturningConstructor.ts, 13, 5))
+>kind : Symbol(kind, Decl(classExtendsConstructorReturningConstructor.ts, 8, 17))
+
diff --git a/tests/baselines/reference/classExtendsConstructorReturningConstructor.types b/tests/baselines/reference/classExtendsConstructorReturningConstructor.types
new file mode 100644
index 0000000000000..528885a00e32f
--- /dev/null
+++ b/tests/baselines/reference/classExtendsConstructorReturningConstructor.types
@@ -0,0 +1,64 @@
+//// [tests/cases/compiler/classExtendsConstructorReturningConstructor.ts] ////
+
+=== classExtendsConstructorReturningConstructor.ts ===
+// https://github.com/microsoft/TypeScript/issues/63559
+// Extending a value whose type is a class instance type carrying a construct
+// signature should resolve the base type from that construct signature rather
+// than repeating the outer instance type.
+
+export declare const Base: {
+>Base : new () => { kind: "A"; new (): { kind: "B"; }; }
+> : ^^^^^^^^^^
+
+ new (): {
+ kind: "A";
+>kind : "A"
+> : ^^^
+
+ new (): { kind: "B" };
+>kind : "B"
+> : ^^^
+
+ };
+};
+
+const A = new class extends Base {}();
+>A : (Anonymous class)
+> : ^^^^^^^^^^^^^^^^^
+>new class extends Base {}() : (Anonymous class)
+> : ^^^^^^^^^^^^^^^^^
+>class extends Base {} : typeof (Anonymous class)
+> : ^^^^^^^^^^^^^^^^^^^^^^^^
+>Base : { new (): { kind: "B"; }; kind: "A"; }
+> : ^^^^^^^^^^ ^^^^^^^^ ^^^
+
+const B = new class extends A {}();
+>B : (Anonymous class)
+> : ^^^^^^^^^^^^^^^^^
+>new class extends A {}() : (Anonymous class)
+> : ^^^^^^^^^^^^^^^^^
+>class extends A {} : typeof (Anonymous class)
+> : ^^^^^^^^^^^^^^^^^^^^^^^^
+>A : { kind: "B"; }
+> : ^^^^^^^^ ^^^
+
+const a: "A" = A.kind;
+>a : "A"
+> : ^^^
+>A.kind : "A"
+> : ^^^
+>A : (Anonymous class)
+> : ^^^^^^^^^^^^^^^^^
+>kind : "A"
+> : ^^^
+
+const b: "B" = B.kind;
+>b : "B"
+> : ^^^
+>B.kind : "B"
+> : ^^^
+>B : (Anonymous class)
+> : ^^^^^^^^^^^^^^^^^
+>kind : "B"
+> : ^^^
+
diff --git a/tests/cases/compiler/classExtendsConstructorReturningConstructor.ts b/tests/cases/compiler/classExtendsConstructorReturningConstructor.ts
new file mode 100644
index 0000000000000..2d497836aeb5e
--- /dev/null
+++ b/tests/cases/compiler/classExtendsConstructorReturningConstructor.ts
@@ -0,0 +1,20 @@
+// @strict: true
+// @noEmit: true
+
+// https://github.com/microsoft/TypeScript/issues/63559
+// Extending a value whose type is a class instance type carrying a construct
+// signature should resolve the base type from that construct signature rather
+// than repeating the outer instance type.
+
+export declare const Base: {
+ new (): {
+ kind: "A";
+ new (): { kind: "B" };
+ };
+};
+
+const A = new class extends Base {}();
+const B = new class extends A {}();
+
+const a: "A" = A.kind;
+const b: "B" = B.kind;