Skip to content

Fix false positive "no overlap" error when comparing this with subclass instances #62070

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 6 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22198,6 +22198,21 @@ 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 (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));
if (related !== undefined) {
Expand Down
57 changes: 57 additions & 0 deletions tests/baselines/reference/thisTypeComparison.js
Original file line number Diff line number Diff line change
@@ -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();
46 changes: 46 additions & 0 deletions tests/baselines/reference/thisTypeComparison.symbols
Original file line number Diff line number Diff line change
@@ -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))

77 changes: 77 additions & 0 deletions tests/baselines/reference/thisTypeComparison.types
Original file line number Diff line number Diff line change
@@ -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
> : ^^^^^^^^^^

66 changes: 66 additions & 0 deletions tests/baselines/reference/thisTypeComparisonExtended.errors.txt
Original file line number Diff line number Diff line change
@@ -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");
}
}
}
Loading
Loading