From 58449bba93c6bc486a8f4f5f81f8689149352f84 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 03:47:34 +0000 Subject: [PATCH 1/6] Initial plan From 6355df5d95c9c0202635cefffe4650d9167ee43a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 03:53:44 +0000 Subject: [PATCH 2/6] Initial exploration and test case creation Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- tests/cases/compiler/thisTypeComparison.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 tests/cases/compiler/thisTypeComparison.ts diff --git a/tests/cases/compiler/thisTypeComparison.ts b/tests/cases/compiler/thisTypeComparison.ts new file mode 100644 index 0000000000000..1bbcae9d03375 --- /dev/null +++ b/tests/cases/compiler/thisTypeComparison.ts @@ -0,0 +1,17 @@ +// @strict: true + +class AA { + do1() { + const b = dd.getB(); + if (this === b) { + console.log("this === b"); + } + } +} + +class BB extends AA { + getB(): BB { return this; } +} + +let dd = new BB(); +dd.do1(); \ No newline at end of file From c3df353a8da37d100d31c44997ffdb5ccaf3780c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 03:59:49 +0000 Subject: [PATCH 3/6] Implement fix for this type comparison false positive Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- src/compiler/checker.ts | 25 ++++-- .../baselines/reference/thisTypeComparison.js | 57 ++++++++++++++ .../reference/thisTypeComparison.symbols | 46 +++++++++++ .../reference/thisTypeComparison.types | 77 +++++++++++++++++++ .../compiler/thisTypeComparisonExtended.ts | 62 +++++++++++++++ 5 files changed, 262 insertions(+), 5 deletions(-) create mode 100644 tests/baselines/reference/thisTypeComparison.js create mode 100644 tests/baselines/reference/thisTypeComparison.symbols create mode 100644 tests/baselines/reference/thisTypeComparison.types create mode 100644 tests/cases/compiler/thisTypeComparisonExtended.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index c5701087ebfd4..35972825d06f1 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22143,11 +22143,26 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { (source as NumberLiteralType).value === (target as NumberLiteralType).value ) return true; if (s & TypeFlags.BigIntLike && t & TypeFlags.BigInt) return true; - if (s & TypeFlags.BooleanLike && t & TypeFlags.Boolean) return true; - if (s & TypeFlags.ESSymbolLike && t & TypeFlags.ESSymbol) return true; - if ( - s & TypeFlags.Enum && t & TypeFlags.Enum && source.symbol.escapedName === target.symbol.escapedName && - isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter) + if (s & TypeFlags.BooleanLike && t & TypeFlags.Boolean) return true; + if (s & TypeFlags.ESSymbolLike && t & TypeFlags.ESSymbol) return true; + // For comparable relation, revert `this` type parameters back to their constrained class type + if (relation === comparableRelation) { + if (s & TypeFlags.TypeParameter && (source as TypeParameter).isThisType) { + const constraint = getConstraintOfTypeParameter(source as TypeParameter); + if (constraint && isTypeRelatedTo(constraint, target, relation)) { + return true; + } + } + if (t & TypeFlags.TypeParameter && (target as TypeParameter).isThisType) { + const constraint = getConstraintOfTypeParameter(target as TypeParameter); + if (constraint && isTypeRelatedTo(source, constraint, relation)) { + return true; + } + } + } + if ( + s & TypeFlags.Enum && t & TypeFlags.Enum && source.symbol.escapedName === target.symbol.escapedName && + isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter) ) return true; if (s & TypeFlags.EnumLiteral && t & TypeFlags.EnumLiteral) { if (s & TypeFlags.Union && t & TypeFlags.Union && isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter)) return true; diff --git a/tests/baselines/reference/thisTypeComparison.js b/tests/baselines/reference/thisTypeComparison.js new file mode 100644 index 0000000000000..67e5b96ad2bc2 --- /dev/null +++ b/tests/baselines/reference/thisTypeComparison.js @@ -0,0 +1,57 @@ +//// [tests/cases/compiler/thisTypeComparison.ts] //// + +//// [thisTypeComparison.ts] +class AA { + do1() { + const b = dd.getB(); + if (this === b) { + console.log("this === b"); + } + } +} + +class BB extends AA { + getB(): BB { return this; } +} + +let dd = new BB(); +dd.do1(); + +//// [thisTypeComparison.js] +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var AA = /** @class */ (function () { + function AA() { + } + AA.prototype.do1 = function () { + var b = dd.getB(); + if (this === b) { + console.log("this === b"); + } + }; + return AA; +}()); +var BB = /** @class */ (function (_super) { + __extends(BB, _super); + function BB() { + return _super !== null && _super.apply(this, arguments) || this; + } + BB.prototype.getB = function () { return this; }; + return BB; +}(AA)); +var dd = new BB(); +dd.do1(); diff --git a/tests/baselines/reference/thisTypeComparison.symbols b/tests/baselines/reference/thisTypeComparison.symbols new file mode 100644 index 0000000000000..22a976a3f35d4 --- /dev/null +++ b/tests/baselines/reference/thisTypeComparison.symbols @@ -0,0 +1,46 @@ +//// [tests/cases/compiler/thisTypeComparison.ts] //// + +=== thisTypeComparison.ts === +class AA { +>AA : Symbol(AA, Decl(thisTypeComparison.ts, 0, 0)) + + do1() { +>do1 : Symbol(AA.do1, Decl(thisTypeComparison.ts, 0, 10)) + + const b = dd.getB(); +>b : Symbol(b, Decl(thisTypeComparison.ts, 2, 13)) +>dd.getB : Symbol(BB.getB, Decl(thisTypeComparison.ts, 9, 21)) +>dd : Symbol(dd, Decl(thisTypeComparison.ts, 13, 3)) +>getB : Symbol(BB.getB, Decl(thisTypeComparison.ts, 9, 21)) + + if (this === b) { +>this : Symbol(AA, Decl(thisTypeComparison.ts, 0, 0)) +>b : Symbol(b, Decl(thisTypeComparison.ts, 2, 13)) + + console.log("this === b"); +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) + } + } +} + +class BB extends AA { +>BB : Symbol(BB, Decl(thisTypeComparison.ts, 7, 1)) +>AA : Symbol(AA, Decl(thisTypeComparison.ts, 0, 0)) + + getB(): BB { return this; } +>getB : Symbol(BB.getB, Decl(thisTypeComparison.ts, 9, 21)) +>BB : Symbol(BB, Decl(thisTypeComparison.ts, 7, 1)) +>this : Symbol(BB, Decl(thisTypeComparison.ts, 7, 1)) +} + +let dd = new BB(); +>dd : Symbol(dd, Decl(thisTypeComparison.ts, 13, 3)) +>BB : Symbol(BB, Decl(thisTypeComparison.ts, 7, 1)) + +dd.do1(); +>dd.do1 : Symbol(AA.do1, Decl(thisTypeComparison.ts, 0, 10)) +>dd : Symbol(dd, Decl(thisTypeComparison.ts, 13, 3)) +>do1 : Symbol(AA.do1, Decl(thisTypeComparison.ts, 0, 10)) + diff --git a/tests/baselines/reference/thisTypeComparison.types b/tests/baselines/reference/thisTypeComparison.types new file mode 100644 index 0000000000000..8c20f569cb06a --- /dev/null +++ b/tests/baselines/reference/thisTypeComparison.types @@ -0,0 +1,77 @@ +//// [tests/cases/compiler/thisTypeComparison.ts] //// + +=== thisTypeComparison.ts === +class AA { +>AA : AA +> : ^^ + + do1() { +>do1 : () => void +> : ^^^^^^^^^^ + + const b = dd.getB(); +>b : BB +> : ^^ +>dd.getB() : BB +> : ^^ +>dd.getB : () => BB +> : ^^^^^^ +>dd : BB +> : ^^ +>getB : () => BB +> : ^^^^^^ + + if (this === b) { +>this === b : boolean +> : ^^^^^^^ +>this : this +> : ^^^^ +>b : BB +> : ^^ + + console.log("this === b"); +>console.log("this === b") : void +> : ^^^^ +>console.log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>console : Console +> : ^^^^^^^ +>log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>"this === b" : "this === b" +> : ^^^^^^^^^^^^ + } + } +} + +class BB extends AA { +>BB : BB +> : ^^ +>AA : AA +> : ^^ + + getB(): BB { return this; } +>getB : () => BB +> : ^^^^^^ +>this : this +> : ^^^^ +} + +let dd = new BB(); +>dd : BB +> : ^^ +>new BB() : BB +> : ^^ +>BB : typeof BB +> : ^^^^^^^^^ + +dd.do1(); +>dd.do1() : void +> : ^^^^ +>dd.do1 : () => void +> : ^^^^^^^^^^ +>dd : BB +> : ^^ +>do1 : () => void +> : ^^^^^^^^^^ + diff --git a/tests/cases/compiler/thisTypeComparisonExtended.ts b/tests/cases/compiler/thisTypeComparisonExtended.ts new file mode 100644 index 0000000000000..8d1845012899a --- /dev/null +++ b/tests/cases/compiler/thisTypeComparisonExtended.ts @@ -0,0 +1,62 @@ +// @strict: true + +// Test 1: Original issue - this === subclass instance should work +class AA { + do1() { + const b = dd.getB(); + if (this === b) { // Should not error + console.log("this === b"); + } + } +} + +class BB extends AA { + getB(): BB { return this; } +} + +let dd = new BB(); +dd.do1(); + +// Test 2: this === unrelated class should still error +class CC { + value: number = 42; +} + +class DD { + test() { + const c = new CC(); + if (this === c) { // Should still error - no relationship + console.log("unrelated"); + } + } +} + +// Test 3: Multiple inheritance levels +class EE extends BB { + getE(): EE { return this; } +} + +class FF extends EE { + testMultiLevel() { + const e = new EE(); + if (this === e) { // Should not error - FF extends EE + console.log("multi-level inheritance"); + } + } +} + +// Test 4: Interface implementation +interface ITest { + getValue(): number; +} + +class GG implements ITest { + getValue() { return 42; } + + testInterface() { + const impl: ITest = new GG(); + if (this === impl) { // Should not error + console.log("interface implementation"); + } + } +} \ No newline at end of file From 5754edce4b1197e259b0a174cbedf9caec783fbe Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 15 Jul 2025 04:04:34 +0000 Subject: [PATCH 4/6] Final fix verification and code formatting Co-authored-by: RyanCavanaugh <6685088+RyanCavanaugh@users.noreply.github.com> --- src/compiler/checker.ts | 40 +-- .../thisTypeComparisonExtended.errors.txt | 66 +++++ .../reference/thisTypeComparisonExtended.js | 155 ++++++++++++ .../thisTypeComparisonExtended.symbols | 145 +++++++++++ .../thisTypeComparisonExtended.types | 235 ++++++++++++++++++ 5 files changed, 621 insertions(+), 20 deletions(-) create mode 100644 tests/baselines/reference/thisTypeComparisonExtended.errors.txt create mode 100644 tests/baselines/reference/thisTypeComparisonExtended.js create mode 100644 tests/baselines/reference/thisTypeComparisonExtended.symbols create mode 100644 tests/baselines/reference/thisTypeComparisonExtended.types diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 35972825d06f1..df88c496070fe 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22143,26 +22143,26 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { (source as NumberLiteralType).value === (target as NumberLiteralType).value ) return true; if (s & TypeFlags.BigIntLike && t & TypeFlags.BigInt) return true; - if (s & TypeFlags.BooleanLike && t & TypeFlags.Boolean) return true; - if (s & TypeFlags.ESSymbolLike && t & TypeFlags.ESSymbol) return true; - // For comparable relation, revert `this` type parameters back to their constrained class type - if (relation === comparableRelation) { - if (s & TypeFlags.TypeParameter && (source as TypeParameter).isThisType) { - const constraint = getConstraintOfTypeParameter(source as TypeParameter); - if (constraint && isTypeRelatedTo(constraint, target, relation)) { - return true; - } - } - if (t & TypeFlags.TypeParameter && (target as TypeParameter).isThisType) { - const constraint = getConstraintOfTypeParameter(target as TypeParameter); - if (constraint && isTypeRelatedTo(source, constraint, relation)) { - return true; - } - } - } - if ( - s & TypeFlags.Enum && t & TypeFlags.Enum && source.symbol.escapedName === target.symbol.escapedName && - isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter) + if (s & TypeFlags.BooleanLike && t & TypeFlags.Boolean) return true; + if (s & TypeFlags.ESSymbolLike && t & TypeFlags.ESSymbol) return true; + // For comparable relation, revert `this` type parameters back to their constrained class type + if (relation === comparableRelation) { + if (s & TypeFlags.TypeParameter && (source as TypeParameter).isThisType) { + const constraint = getConstraintOfTypeParameter(source as TypeParameter); + if (constraint && isTypeRelatedTo(constraint, target, relation)) { + return true; + } + } + if (t & TypeFlags.TypeParameter && (target as TypeParameter).isThisType) { + const constraint = getConstraintOfTypeParameter(target as TypeParameter); + if (constraint && isTypeRelatedTo(source, constraint, relation)) { + return true; + } + } + } + if ( + s & TypeFlags.Enum && t & TypeFlags.Enum && source.symbol.escapedName === target.symbol.escapedName && + isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter) ) return true; if (s & TypeFlags.EnumLiteral && t & TypeFlags.EnumLiteral) { if (s & TypeFlags.Union && t & TypeFlags.Union && isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter)) return true; diff --git a/tests/baselines/reference/thisTypeComparisonExtended.errors.txt b/tests/baselines/reference/thisTypeComparisonExtended.errors.txt new file mode 100644 index 0000000000000..31b0051c7e27d --- /dev/null +++ b/tests/baselines/reference/thisTypeComparisonExtended.errors.txt @@ -0,0 +1,66 @@ +thisTypeComparisonExtended.ts(26,13): error TS2367: This comparison appears to be unintentional because the types 'this' and 'CC' have no overlap. + + +==== thisTypeComparisonExtended.ts (1 errors) ==== + // Test 1: Original issue - this === subclass instance should work + class AA { + do1() { + const b = dd.getB(); + if (this === b) { // Should not error + console.log("this === b"); + } + } + } + + class BB extends AA { + getB(): BB { return this; } + } + + let dd = new BB(); + dd.do1(); + + // Test 2: this === unrelated class should still error + class CC { + value: number = 42; + } + + class DD { + test() { + const c = new CC(); + if (this === c) { // Should still error - no relationship + ~~~~~~~~~~ +!!! error TS2367: This comparison appears to be unintentional because the types 'this' and 'CC' have no overlap. + console.log("unrelated"); + } + } + } + + // Test 3: Multiple inheritance levels + class EE extends BB { + getE(): EE { return this; } + } + + class FF extends EE { + testMultiLevel() { + const e = new EE(); + if (this === e) { // Should not error - FF extends EE + console.log("multi-level inheritance"); + } + } + } + + // Test 4: Interface implementation + interface ITest { + getValue(): number; + } + + class GG implements ITest { + getValue() { return 42; } + + testInterface() { + const impl: ITest = new GG(); + if (this === impl) { // Should not error + console.log("interface implementation"); + } + } + } \ No newline at end of file diff --git a/tests/baselines/reference/thisTypeComparisonExtended.js b/tests/baselines/reference/thisTypeComparisonExtended.js new file mode 100644 index 0000000000000..661d9c93f5b0d --- /dev/null +++ b/tests/baselines/reference/thisTypeComparisonExtended.js @@ -0,0 +1,155 @@ +//// [tests/cases/compiler/thisTypeComparisonExtended.ts] //// + +//// [thisTypeComparisonExtended.ts] +// Test 1: Original issue - this === subclass instance should work +class AA { + do1() { + const b = dd.getB(); + if (this === b) { // Should not error + console.log("this === b"); + } + } +} + +class BB extends AA { + getB(): BB { return this; } +} + +let dd = new BB(); +dd.do1(); + +// Test 2: this === unrelated class should still error +class CC { + value: number = 42; +} + +class DD { + test() { + const c = new CC(); + if (this === c) { // Should still error - no relationship + console.log("unrelated"); + } + } +} + +// Test 3: Multiple inheritance levels +class EE extends BB { + getE(): EE { return this; } +} + +class FF extends EE { + testMultiLevel() { + const e = new EE(); + if (this === e) { // Should not error - FF extends EE + console.log("multi-level inheritance"); + } + } +} + +// Test 4: Interface implementation +interface ITest { + getValue(): number; +} + +class GG implements ITest { + getValue() { return 42; } + + testInterface() { + const impl: ITest = new GG(); + if (this === impl) { // Should not error + console.log("interface implementation"); + } + } +} + +//// [thisTypeComparisonExtended.js] +"use strict"; +var __extends = (this && this.__extends) || (function () { + var extendStatics = function (d, b) { + extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p]; }; + return extendStatics(d, b); + }; + return function (d, b) { + if (typeof b !== "function" && b !== null) + throw new TypeError("Class extends value " + String(b) + " is not a constructor or null"); + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +// Test 1: Original issue - this === subclass instance should work +var AA = /** @class */ (function () { + function AA() { + } + AA.prototype.do1 = function () { + var b = dd.getB(); + if (this === b) { // Should not error + console.log("this === b"); + } + }; + return AA; +}()); +var BB = /** @class */ (function (_super) { + __extends(BB, _super); + function BB() { + return _super !== null && _super.apply(this, arguments) || this; + } + BB.prototype.getB = function () { return this; }; + return BB; +}(AA)); +var dd = new BB(); +dd.do1(); +// Test 2: this === unrelated class should still error +var CC = /** @class */ (function () { + function CC() { + this.value = 42; + } + return CC; +}()); +var DD = /** @class */ (function () { + function DD() { + } + DD.prototype.test = function () { + var c = new CC(); + if (this === c) { // Should still error - no relationship + console.log("unrelated"); + } + }; + return DD; +}()); +// Test 3: Multiple inheritance levels +var EE = /** @class */ (function (_super) { + __extends(EE, _super); + function EE() { + return _super !== null && _super.apply(this, arguments) || this; + } + EE.prototype.getE = function () { return this; }; + return EE; +}(BB)); +var FF = /** @class */ (function (_super) { + __extends(FF, _super); + function FF() { + return _super !== null && _super.apply(this, arguments) || this; + } + FF.prototype.testMultiLevel = function () { + var e = new EE(); + if (this === e) { // Should not error - FF extends EE + console.log("multi-level inheritance"); + } + }; + return FF; +}(EE)); +var GG = /** @class */ (function () { + function GG() { + } + GG.prototype.getValue = function () { return 42; }; + GG.prototype.testInterface = function () { + var impl = new GG(); + if (this === impl) { // Should not error + console.log("interface implementation"); + } + }; + return GG; +}()); diff --git a/tests/baselines/reference/thisTypeComparisonExtended.symbols b/tests/baselines/reference/thisTypeComparisonExtended.symbols new file mode 100644 index 0000000000000..c42a734c98b2c --- /dev/null +++ b/tests/baselines/reference/thisTypeComparisonExtended.symbols @@ -0,0 +1,145 @@ +//// [tests/cases/compiler/thisTypeComparisonExtended.ts] //// + +=== thisTypeComparisonExtended.ts === +// Test 1: Original issue - this === subclass instance should work +class AA { +>AA : Symbol(AA, Decl(thisTypeComparisonExtended.ts, 0, 0)) + + do1() { +>do1 : Symbol(AA.do1, Decl(thisTypeComparisonExtended.ts, 1, 10)) + + const b = dd.getB(); +>b : Symbol(b, Decl(thisTypeComparisonExtended.ts, 3, 13)) +>dd.getB : Symbol(BB.getB, Decl(thisTypeComparisonExtended.ts, 10, 21)) +>dd : Symbol(dd, Decl(thisTypeComparisonExtended.ts, 14, 3)) +>getB : Symbol(BB.getB, Decl(thisTypeComparisonExtended.ts, 10, 21)) + + if (this === b) { // Should not error +>this : Symbol(AA, Decl(thisTypeComparisonExtended.ts, 0, 0)) +>b : Symbol(b, Decl(thisTypeComparisonExtended.ts, 3, 13)) + + console.log("this === b"); +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) + } + } +} + +class BB extends AA { +>BB : Symbol(BB, Decl(thisTypeComparisonExtended.ts, 8, 1)) +>AA : Symbol(AA, Decl(thisTypeComparisonExtended.ts, 0, 0)) + + getB(): BB { return this; } +>getB : Symbol(BB.getB, Decl(thisTypeComparisonExtended.ts, 10, 21)) +>BB : Symbol(BB, Decl(thisTypeComparisonExtended.ts, 8, 1)) +>this : Symbol(BB, Decl(thisTypeComparisonExtended.ts, 8, 1)) +} + +let dd = new BB(); +>dd : Symbol(dd, Decl(thisTypeComparisonExtended.ts, 14, 3)) +>BB : Symbol(BB, Decl(thisTypeComparisonExtended.ts, 8, 1)) + +dd.do1(); +>dd.do1 : Symbol(AA.do1, Decl(thisTypeComparisonExtended.ts, 1, 10)) +>dd : Symbol(dd, Decl(thisTypeComparisonExtended.ts, 14, 3)) +>do1 : Symbol(AA.do1, Decl(thisTypeComparisonExtended.ts, 1, 10)) + +// Test 2: this === unrelated class should still error +class CC { +>CC : Symbol(CC, Decl(thisTypeComparisonExtended.ts, 15, 9)) + + value: number = 42; +>value : Symbol(CC.value, Decl(thisTypeComparisonExtended.ts, 18, 10)) +} + +class DD { +>DD : Symbol(DD, Decl(thisTypeComparisonExtended.ts, 20, 1)) + + test() { +>test : Symbol(DD.test, Decl(thisTypeComparisonExtended.ts, 22, 10)) + + const c = new CC(); +>c : Symbol(c, Decl(thisTypeComparisonExtended.ts, 24, 13)) +>CC : Symbol(CC, Decl(thisTypeComparisonExtended.ts, 15, 9)) + + if (this === c) { // Should still error - no relationship +>this : Symbol(DD, Decl(thisTypeComparisonExtended.ts, 20, 1)) +>c : Symbol(c, Decl(thisTypeComparisonExtended.ts, 24, 13)) + + console.log("unrelated"); +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) + } + } +} + +// Test 3: Multiple inheritance levels +class EE extends BB { +>EE : Symbol(EE, Decl(thisTypeComparisonExtended.ts, 29, 1)) +>BB : Symbol(BB, Decl(thisTypeComparisonExtended.ts, 8, 1)) + + getE(): EE { return this; } +>getE : Symbol(EE.getE, Decl(thisTypeComparisonExtended.ts, 32, 21)) +>EE : Symbol(EE, Decl(thisTypeComparisonExtended.ts, 29, 1)) +>this : Symbol(EE, Decl(thisTypeComparisonExtended.ts, 29, 1)) +} + +class FF extends EE { +>FF : Symbol(FF, Decl(thisTypeComparisonExtended.ts, 34, 1)) +>EE : Symbol(EE, Decl(thisTypeComparisonExtended.ts, 29, 1)) + + testMultiLevel() { +>testMultiLevel : Symbol(FF.testMultiLevel, Decl(thisTypeComparisonExtended.ts, 36, 21)) + + const e = new EE(); +>e : Symbol(e, Decl(thisTypeComparisonExtended.ts, 38, 13)) +>EE : Symbol(EE, Decl(thisTypeComparisonExtended.ts, 29, 1)) + + if (this === e) { // Should not error - FF extends EE +>this : Symbol(FF, Decl(thisTypeComparisonExtended.ts, 34, 1)) +>e : Symbol(e, Decl(thisTypeComparisonExtended.ts, 38, 13)) + + console.log("multi-level inheritance"); +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) + } + } +} + +// Test 4: Interface implementation +interface ITest { +>ITest : Symbol(ITest, Decl(thisTypeComparisonExtended.ts, 43, 1)) + + getValue(): number; +>getValue : Symbol(ITest.getValue, Decl(thisTypeComparisonExtended.ts, 46, 17)) +} + +class GG implements ITest { +>GG : Symbol(GG, Decl(thisTypeComparisonExtended.ts, 48, 1)) +>ITest : Symbol(ITest, Decl(thisTypeComparisonExtended.ts, 43, 1)) + + getValue() { return 42; } +>getValue : Symbol(GG.getValue, Decl(thisTypeComparisonExtended.ts, 50, 27)) + + testInterface() { +>testInterface : Symbol(GG.testInterface, Decl(thisTypeComparisonExtended.ts, 51, 29)) + + const impl: ITest = new GG(); +>impl : Symbol(impl, Decl(thisTypeComparisonExtended.ts, 54, 13)) +>ITest : Symbol(ITest, Decl(thisTypeComparisonExtended.ts, 43, 1)) +>GG : Symbol(GG, Decl(thisTypeComparisonExtended.ts, 48, 1)) + + if (this === impl) { // Should not error +>this : Symbol(GG, Decl(thisTypeComparisonExtended.ts, 48, 1)) +>impl : Symbol(impl, Decl(thisTypeComparisonExtended.ts, 54, 13)) + + console.log("interface implementation"); +>console.log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) +>console : Symbol(console, Decl(lib.dom.d.ts, --, --)) +>log : Symbol(Console.log, Decl(lib.dom.d.ts, --, --)) + } + } +} diff --git a/tests/baselines/reference/thisTypeComparisonExtended.types b/tests/baselines/reference/thisTypeComparisonExtended.types new file mode 100644 index 0000000000000..2d112a9bc5257 --- /dev/null +++ b/tests/baselines/reference/thisTypeComparisonExtended.types @@ -0,0 +1,235 @@ +//// [tests/cases/compiler/thisTypeComparisonExtended.ts] //// + +=== thisTypeComparisonExtended.ts === +// Test 1: Original issue - this === subclass instance should work +class AA { +>AA : AA +> : ^^ + + do1() { +>do1 : () => void +> : ^^^^^^^^^^ + + const b = dd.getB(); +>b : BB +> : ^^ +>dd.getB() : BB +> : ^^ +>dd.getB : () => BB +> : ^^^^^^ +>dd : BB +> : ^^ +>getB : () => BB +> : ^^^^^^ + + if (this === b) { // Should not error +>this === b : boolean +> : ^^^^^^^ +>this : this +> : ^^^^ +>b : BB +> : ^^ + + console.log("this === b"); +>console.log("this === b") : void +> : ^^^^ +>console.log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>console : Console +> : ^^^^^^^ +>log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>"this === b" : "this === b" +> : ^^^^^^^^^^^^ + } + } +} + +class BB extends AA { +>BB : BB +> : ^^ +>AA : AA +> : ^^ + + getB(): BB { return this; } +>getB : () => BB +> : ^^^^^^ +>this : this +> : ^^^^ +} + +let dd = new BB(); +>dd : BB +> : ^^ +>new BB() : BB +> : ^^ +>BB : typeof BB +> : ^^^^^^^^^ + +dd.do1(); +>dd.do1() : void +> : ^^^^ +>dd.do1 : () => void +> : ^^^^^^^^^^ +>dd : BB +> : ^^ +>do1 : () => void +> : ^^^^^^^^^^ + +// Test 2: this === unrelated class should still error +class CC { +>CC : CC +> : ^^ + + value: number = 42; +>value : number +> : ^^^^^^ +>42 : 42 +> : ^^ +} + +class DD { +>DD : DD +> : ^^ + + test() { +>test : () => void +> : ^^^^^^^^^^ + + const c = new CC(); +>c : CC +> : ^^ +>new CC() : CC +> : ^^ +>CC : typeof CC +> : ^^^^^^^^^ + + if (this === c) { // Should still error - no relationship +>this === c : boolean +> : ^^^^^^^ +>this : this +> : ^^^^ +>c : CC +> : ^^ + + console.log("unrelated"); +>console.log("unrelated") : void +> : ^^^^ +>console.log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>console : Console +> : ^^^^^^^ +>log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>"unrelated" : "unrelated" +> : ^^^^^^^^^^^ + } + } +} + +// Test 3: Multiple inheritance levels +class EE extends BB { +>EE : EE +> : ^^ +>BB : BB +> : ^^ + + getE(): EE { return this; } +>getE : () => EE +> : ^^^^^^ +>this : this +> : ^^^^ +} + +class FF extends EE { +>FF : FF +> : ^^ +>EE : EE +> : ^^ + + testMultiLevel() { +>testMultiLevel : () => void +> : ^^^^^^^^^^ + + const e = new EE(); +>e : EE +> : ^^ +>new EE() : EE +> : ^^ +>EE : typeof EE +> : ^^^^^^^^^ + + if (this === e) { // Should not error - FF extends EE +>this === e : boolean +> : ^^^^^^^ +>this : this +> : ^^^^ +>e : EE +> : ^^ + + console.log("multi-level inheritance"); +>console.log("multi-level inheritance") : void +> : ^^^^ +>console.log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>console : Console +> : ^^^^^^^ +>log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>"multi-level inheritance" : "multi-level inheritance" +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ + } + } +} + +// Test 4: Interface implementation +interface ITest { + getValue(): number; +>getValue : () => number +> : ^^^^^^ +} + +class GG implements ITest { +>GG : GG +> : ^^ + + getValue() { return 42; } +>getValue : () => number +> : ^^^^^^^^^^^^ +>42 : 42 +> : ^^ + + testInterface() { +>testInterface : () => void +> : ^^^^^^^^^^ + + const impl: ITest = new GG(); +>impl : ITest +> : ^^^^^ +>new GG() : GG +> : ^^ +>GG : typeof GG +> : ^^^^^^^^^ + + if (this === impl) { // Should not error +>this === impl : boolean +> : ^^^^^^^ +>this : this +> : ^^^^ +>impl : ITest +> : ^^^^^ + + console.log("interface implementation"); +>console.log("interface implementation") : void +> : ^^^^ +>console.log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>console : Console +> : ^^^^^^^ +>log : (...data: any[]) => void +> : ^^^^ ^^ ^^^^^ +>"interface implementation" : "interface implementation" +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^ + } + } +} From ba8ae71998697cf9750563e5bd21a7dbe05cecf2 Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 15 Jul 2025 09:20:17 -0700 Subject: [PATCH 5/6] Move this check and remove unnecessary symmetry --- src/compiler/checker.ts | 24 +++++++++--------------- 1 file changed, 9 insertions(+), 15 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index df88c496070fe..6292f41500962 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22145,21 +22145,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (s & TypeFlags.BigIntLike && t & TypeFlags.BigInt) return true; if (s & TypeFlags.BooleanLike && t & TypeFlags.Boolean) return true; if (s & TypeFlags.ESSymbolLike && t & TypeFlags.ESSymbol) return true; - // For comparable relation, revert `this` type parameters back to their constrained class type - if (relation === comparableRelation) { - if (s & TypeFlags.TypeParameter && (source as TypeParameter).isThisType) { - const constraint = getConstraintOfTypeParameter(source as TypeParameter); - if (constraint && isTypeRelatedTo(constraint, target, relation)) { - return true; - } - } - if (t & TypeFlags.TypeParameter && (target as TypeParameter).isThisType) { - const constraint = getConstraintOfTypeParameter(target as TypeParameter); - if (constraint && isTypeRelatedTo(source, constraint, relation)) { - return true; - } - } - } if ( s & TypeFlags.Enum && t & TypeFlags.Enum && source.symbol.escapedName === target.symbol.escapedName && isEnumTypeRelatedTo(source.symbol, target.symbol, errorReporter) @@ -22213,6 +22198,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { if (source.flags !== target.flags) return false; if (source.flags & TypeFlags.Singleton) return true; } + if (relation === comparableRelation) { + // Allow comparability between 'this' and derived classes + if (source.flags & TypeFlags.TypeParameter && (source as TypeParameter).isThisType) { + const constraint = getConstraintOfTypeParameter(source as TypeParameter); + if (constraint && isTypeRelatedTo(constraint, target, relation)) { + return true; + } + } + } if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Object) { const related = relation.get(getRelationKey(source, target, IntersectionState.None, relation, /*ignoreConstraints*/ false)); if (related !== undefined) { From dd2d1c5e6b28beef218e5ec66aa61fd503d3e09d Mon Sep 17 00:00:00 2001 From: Ryan Cavanaugh Date: Tue, 15 Jul 2025 09:48:09 -0700 Subject: [PATCH 6/6] Was actually needed --- src/compiler/checker.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 6292f41500962..9cc1abb65a3a7 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -22206,6 +22206,12 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return true; } } + if (target.flags & TypeFlags.TypeParameter && (target as TypeParameter).isThisType) { + const constraint = getConstraintOfTypeParameter(target as TypeParameter); + if (constraint && isTypeRelatedTo(source, constraint, relation)) { + return true; + } + } } if (source.flags & TypeFlags.Object && target.flags & TypeFlags.Object) { const related = relation.get(getRelationKey(source, target, IntersectionState.None, relation, /*ignoreConstraints*/ false));