From c2c4e139acf5c9e5eaf5f4f7981a24de0f4a162c Mon Sep 17 00:00:00 2001 From: Tanzim Hossain Romel Date: Tue, 30 Dec 2025 16:38:05 +0600 Subject: [PATCH] Fix type parameter leak when using 'this' in reverse mapped types Fixes #62779 When object literal methods reference `this` inside a function with a reverse mapped type parameter, the type parameter T was leaking through. For example: ```typescript declare function test>(obj: { [K in keyof T]: () => T[K]; }): T; const obj = test({ a() { return 0; }, b() { return this.a(); }, }); // Was: { a: number; b: T[string]; } (widened to unknown) // Now: { a: number; b: number; } ``` The fix modifies `getContextualThisParameterType` to use the actual object literal type when in an inference context and the contextual type contains a mapped type. This allows methods to see each other's already-inferred types when resolving `this` references. This also handles intersection and union types containing mapped types. --- src/compiler/checker.ts | 7 + .../reverseMappedThisTypeInference.js | 76 +++++++++ .../reverseMappedThisTypeInference.symbols | 119 ++++++++++++++ .../reverseMappedThisTypeInference.types | 147 ++++++++++++++++++ .../reverseMappedThisTypeInference.ts | 47 ++++++ 5 files changed, 396 insertions(+) create mode 100644 tests/baselines/reference/reverseMappedThisTypeInference.js create mode 100644 tests/baselines/reference/reverseMappedThisTypeInference.symbols create mode 100644 tests/baselines/reference/reverseMappedThisTypeInference.types create mode 100644 tests/cases/compiler/reverseMappedThisTypeInference.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 8c52e9d172877..80a0ec6c51608 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -31825,6 +31825,13 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { // There was no contextual ThisType for the containing object literal, so the contextual type // for 'this' is the non-null form of the contextual type for the containing object literal or // the type of the object literal itself. + // If we're in an inference context and the contextual type contains a mapped type (like reverse + // mapped types used for inference), use the object literal type to allow proper inference + // of method return types that reference other methods via this. This avoids leaking type + // parameters like T[K] when methods call each other. + if (contextualType && someType(contextualType, t => !!(getObjectFlags(t) & ObjectFlags.Mapped)) && getInferenceContext(containingLiteral)) { + return getWidenedType(checkExpressionCached(containingLiteral)); + } return getWidenedType(contextualType ? getNonNullableType(contextualType) : checkExpressionCached(containingLiteral)); } // In an assignment of the form 'obj.xxx = function(...)' or 'obj[xxx] = function(...)', the diff --git a/tests/baselines/reference/reverseMappedThisTypeInference.js b/tests/baselines/reference/reverseMappedThisTypeInference.js new file mode 100644 index 0000000000000..4d474bc6cda14 --- /dev/null +++ b/tests/baselines/reference/reverseMappedThisTypeInference.js @@ -0,0 +1,76 @@ +//// [tests/cases/compiler/reverseMappedThisTypeInference.ts] //// + +//// [reverseMappedThisTypeInference.ts] +// Issue #62779: Type parameter leak caused by `this` and reverse mapped type +declare function testReverseMapped>(obj: { + [K in keyof T]: () => T[K]; +}): T; + +const obj = testReverseMapped({ + a() { + return 0; + }, + b() { + return this.a(); + }, +}); + +// Intersection with mapped type +declare function testReverseMapped2, T2>( + obj: T2 & { + [K in keyof T]: () => T[K]; + }, +): T; + +const obj2 = testReverseMapped2({ + a() { + return 0; + }, + b() { + return this.a(); + }, +}); + +// Union with mapped type +declare function testReverseMapped3, T2>( + obj: T2 | { + [K in keyof T]: () => T[K]; + }, +): T; + +const obj3 = testReverseMapped3({ + a() { + return 0; + }, + b() { + return this.a(); + }, +}); + + +//// [reverseMappedThisTypeInference.js] +"use strict"; +var obj = testReverseMapped({ + a: function () { + return 0; + }, + b: function () { + return this.a(); + }, +}); +var obj2 = testReverseMapped2({ + a: function () { + return 0; + }, + b: function () { + return this.a(); + }, +}); +var obj3 = testReverseMapped3({ + a: function () { + return 0; + }, + b: function () { + return this.a(); + }, +}); diff --git a/tests/baselines/reference/reverseMappedThisTypeInference.symbols b/tests/baselines/reference/reverseMappedThisTypeInference.symbols new file mode 100644 index 0000000000000..2681146800b7a --- /dev/null +++ b/tests/baselines/reference/reverseMappedThisTypeInference.symbols @@ -0,0 +1,119 @@ +//// [tests/cases/compiler/reverseMappedThisTypeInference.ts] //// + +=== reverseMappedThisTypeInference.ts === +// Issue #62779: Type parameter leak caused by `this` and reverse mapped type +declare function testReverseMapped>(obj: { +>testReverseMapped : Symbol(testReverseMapped, Decl(reverseMappedThisTypeInference.ts, 0, 0)) +>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 1, 35)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +>obj : Symbol(obj, Decl(reverseMappedThisTypeInference.ts, 1, 70)) + + [K in keyof T]: () => T[K]; +>K : Symbol(K, Decl(reverseMappedThisTypeInference.ts, 2, 5)) +>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 1, 35)) +>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 1, 35)) +>K : Symbol(K, Decl(reverseMappedThisTypeInference.ts, 2, 5)) + +}): T; +>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 1, 35)) + +const obj = testReverseMapped({ +>obj : Symbol(obj, Decl(reverseMappedThisTypeInference.ts, 5, 5)) +>testReverseMapped : Symbol(testReverseMapped, Decl(reverseMappedThisTypeInference.ts, 0, 0)) + + a() { +>a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 5, 31)) + + return 0; + }, + b() { +>b : Symbol(b, Decl(reverseMappedThisTypeInference.ts, 8, 6)) + + return this.a(); +>this.a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 5, 31)) +>this : Symbol(__type, Decl(reverseMappedThisTypeInference.ts, 1, 74)) +>a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 5, 31)) + + }, +}); + +// Intersection with mapped type +declare function testReverseMapped2, T2>( +>testReverseMapped2 : Symbol(testReverseMapped2, Decl(reverseMappedThisTypeInference.ts, 12, 3)) +>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 15, 36)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +>T2 : Symbol(T2, Decl(reverseMappedThisTypeInference.ts, 15, 70)) + + obj: T2 & { +>obj : Symbol(obj, Decl(reverseMappedThisTypeInference.ts, 15, 75)) +>T2 : Symbol(T2, Decl(reverseMappedThisTypeInference.ts, 15, 70)) + + [K in keyof T]: () => T[K]; +>K : Symbol(K, Decl(reverseMappedThisTypeInference.ts, 17, 9)) +>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 15, 36)) +>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 15, 36)) +>K : Symbol(K, Decl(reverseMappedThisTypeInference.ts, 17, 9)) + + }, +): T; +>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 15, 36)) + +const obj2 = testReverseMapped2({ +>obj2 : Symbol(obj2, Decl(reverseMappedThisTypeInference.ts, 21, 5)) +>testReverseMapped2 : Symbol(testReverseMapped2, Decl(reverseMappedThisTypeInference.ts, 12, 3)) + + a() { +>a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 21, 33)) + + return 0; + }, + b() { +>b : Symbol(b, Decl(reverseMappedThisTypeInference.ts, 24, 6)) + + return this.a(); +>this.a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 21, 33), Decl(reverseMappedThisTypeInference.ts, 21, 33)) +>a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 21, 33), Decl(reverseMappedThisTypeInference.ts, 21, 33)) + + }, +}); + +// Union with mapped type +declare function testReverseMapped3, T2>( +>testReverseMapped3 : Symbol(testReverseMapped3, Decl(reverseMappedThisTypeInference.ts, 28, 3)) +>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 31, 36)) +>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --)) +>T2 : Symbol(T2, Decl(reverseMappedThisTypeInference.ts, 31, 70)) + + obj: T2 | { +>obj : Symbol(obj, Decl(reverseMappedThisTypeInference.ts, 31, 75)) +>T2 : Symbol(T2, Decl(reverseMappedThisTypeInference.ts, 31, 70)) + + [K in keyof T]: () => T[K]; +>K : Symbol(K, Decl(reverseMappedThisTypeInference.ts, 33, 9)) +>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 31, 36)) +>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 31, 36)) +>K : Symbol(K, Decl(reverseMappedThisTypeInference.ts, 33, 9)) + + }, +): T; +>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 31, 36)) + +const obj3 = testReverseMapped3({ +>obj3 : Symbol(obj3, Decl(reverseMappedThisTypeInference.ts, 37, 5)) +>testReverseMapped3 : Symbol(testReverseMapped3, Decl(reverseMappedThisTypeInference.ts, 28, 3)) + + a() { +>a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 37, 33)) + + return 0; + }, + b() { +>b : Symbol(b, Decl(reverseMappedThisTypeInference.ts, 40, 6)) + + return this.a(); +>this.a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 37, 33), Decl(reverseMappedThisTypeInference.ts, 37, 33)) +>a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 37, 33), Decl(reverseMappedThisTypeInference.ts, 37, 33)) + + }, +}); + diff --git a/tests/baselines/reference/reverseMappedThisTypeInference.types b/tests/baselines/reference/reverseMappedThisTypeInference.types new file mode 100644 index 0000000000000..f9e09489edbf1 --- /dev/null +++ b/tests/baselines/reference/reverseMappedThisTypeInference.types @@ -0,0 +1,147 @@ +//// [tests/cases/compiler/reverseMappedThisTypeInference.ts] //// + +=== reverseMappedThisTypeInference.ts === +// Issue #62779: Type parameter leak caused by `this` and reverse mapped type +declare function testReverseMapped>(obj: { +>testReverseMapped : >(obj: { [K in keyof T]: () => T[K]; }) => T +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>obj : { [K in keyof T]: () => T[K]; } +> : ^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^ + + [K in keyof T]: () => T[K]; +}): T; + +const obj = testReverseMapped({ +>obj : { a: number; b: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>testReverseMapped({ a() { return 0; }, b() { return this.a(); },}) : { a: number; b: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>testReverseMapped : >(obj: { [K in keyof T]: () => T[K]; }) => T +> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^ +>{ a() { return 0; }, b() { return this.a(); },} : { a(): number; b(): number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + a() { +>a : () => number +> : ^^^^^^^^^^^^ + + return 0; +>0 : 0 +> : ^ + + }, + b() { +>b : () => number +> : ^^^^^^^^^^^^ + + return this.a(); +>this.a() : number +> : ^^^^^^ +>this.a : () => number +> : ^^^^^^^^^^^^ +>this : { a: () => number; b: () => number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>a : () => number +> : ^^^^^^^^^^^^ + + }, +}); + +// Intersection with mapped type +declare function testReverseMapped2, T2>( +>testReverseMapped2 : , T2>(obj: T2 & { [K in keyof T]: () => T[K]; }) => T +> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^^^^ + + obj: T2 & { +>obj : T2 & { [K in keyof T]: () => T[K]; } +> : ^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^ + + [K in keyof T]: () => T[K]; + }, +): T; + +const obj2 = testReverseMapped2({ +>obj2 : { a: number; b: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>testReverseMapped2({ a() { return 0; }, b() { return this.a(); },}) : { a: number; b: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>testReverseMapped2 : , T2>(obj: T2 & { [K in keyof T]: () => T[K]; }) => T +> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^^^^ +>{ a() { return 0; }, b() { return this.a(); },} : { a(): number; b(): number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + a() { +>a : () => number +> : ^^^^^^^^^^^^ + + return 0; +>0 : 0 +> : ^ + + }, + b() { +>b : () => number +> : ^^^^^^^^^^^^ + + return this.a(); +>this.a() : number +> : ^^^^^^ +>this.a : (() => number) & (() => number) +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>this : { a(): number; b(): number; } & { a: () => number; b: () => number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>a : (() => number) & (() => number) +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + }, +}); + +// Union with mapped type +declare function testReverseMapped3, T2>( +>testReverseMapped3 : , T2>(obj: T2 | { [K in keyof T]: () => T[K]; }) => T +> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^^^^ + + obj: T2 | { +>obj : T2 | { [K in keyof T]: () => T[K]; } +> : ^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^ + + [K in keyof T]: () => T[K]; + }, +): T; + +const obj3 = testReverseMapped3({ +>obj3 : { a: number; b: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>testReverseMapped3({ a() { return 0; }, b() { return this.a(); },}) : { a: number; b: number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^ +>testReverseMapped3 : , T2>(obj: T2 | { [K in keyof T]: () => T[K]; }) => T +> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^^^^ +>{ a() { return 0; }, b() { return this.a(); },} : { a(): number; b(): number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + a() { +>a : () => number +> : ^^^^^^^^^^^^ + + return 0; +>0 : 0 +> : ^ + + }, + b() { +>b : () => number +> : ^^^^^^^^^^^^ + + return this.a(); +>this.a() : number +> : ^^^^^^ +>this.a : (() => number) | (() => number) +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>this : { a(): number; b(): number; } | { a: () => number; b: () => number; } +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +>a : (() => number) | (() => number) +> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + + }, +}); + diff --git a/tests/cases/compiler/reverseMappedThisTypeInference.ts b/tests/cases/compiler/reverseMappedThisTypeInference.ts new file mode 100644 index 0000000000000..0eaefa024106b --- /dev/null +++ b/tests/cases/compiler/reverseMappedThisTypeInference.ts @@ -0,0 +1,47 @@ +// @strict: true + +// Issue #62779: Type parameter leak caused by `this` and reverse mapped type +declare function testReverseMapped>(obj: { + [K in keyof T]: () => T[K]; +}): T; + +const obj = testReverseMapped({ + a() { + return 0; + }, + b() { + return this.a(); + }, +}); + +// Intersection with mapped type +declare function testReverseMapped2, T2>( + obj: T2 & { + [K in keyof T]: () => T[K]; + }, +): T; + +const obj2 = testReverseMapped2({ + a() { + return 0; + }, + b() { + return this.a(); + }, +}); + +// Union with mapped type +declare function testReverseMapped3, T2>( + obj: T2 | { + [K in keyof T]: () => T[K]; + }, +): T; + +const obj3 = testReverseMapped3({ + a() { + return 0; + }, + b() { + return this.a(); + }, +});