Skip to content

Commit 602ed22

Browse files
committed
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<T extends Record<string, unknown>>(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, rather than the contextual mapped type with unresolved type parameters. This allows methods to see each other's already-inferred types when resolving `this` references.
1 parent 0a07132 commit 602ed22

File tree

5 files changed

+154
-0
lines changed

5 files changed

+154
-0
lines changed

src/compiler/checker.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31825,6 +31825,15 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3182531825
// There was no contextual ThisType<T> for the containing object literal, so the contextual type
3182631826
// for 'this' is the non-null form of the contextual type for the containing object literal or
3182731827
// the type of the object literal itself.
31828+
// If we're in an inference context with a reverse mapped type, use the object literal type
31829+
// to allow proper inference of method return types that reference other methods via this.
31830+
// This avoids leaking type parameters like T[K] when methods call each other.
31831+
const inferenceContext = getInferenceContext(containingLiteral);
31832+
if (inferenceContext) {
31833+
// Use the object literal type directly to break the cycle with the contextual type.
31834+
// This allows methods to see each other's inferred types.
31835+
return getWidenedType(checkExpressionCached(containingLiteral));
31836+
}
3182831837
return getWidenedType(contextualType ? getNonNullableType(contextualType) : checkExpressionCached(containingLiteral));
3182931838
}
3183031839
// In an assignment of the form 'obj.xxx = function(...)' or 'obj[xxx] = function(...)', the
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
//// [tests/cases/compiler/reverseMappedThisTypeInference.ts] ////
2+
3+
//// [reverseMappedThisTypeInference.ts]
4+
// Issue #62779: Type parameter leak caused by `this` and reverse mapped type
5+
declare function testReverseMapped<T extends Record<string, unknown>>(obj: {
6+
[K in keyof T]: () => T[K];
7+
}): T;
8+
9+
const obj = testReverseMapped({
10+
a() {
11+
return 0;
12+
},
13+
b() {
14+
return this.a();
15+
},
16+
});
17+
18+
// Expected: { a: number; b: number; }
19+
// Actual bug: { a: number; b: T[string]; }
20+
21+
22+
//// [reverseMappedThisTypeInference.js]
23+
"use strict";
24+
var obj = testReverseMapped({
25+
a: function () {
26+
return 0;
27+
},
28+
b: function () {
29+
return this.a();
30+
},
31+
});
32+
// Expected: { a: number; b: number; }
33+
// Actual bug: { a: number; b: T[string]; }
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
//// [tests/cases/compiler/reverseMappedThisTypeInference.ts] ////
2+
3+
=== reverseMappedThisTypeInference.ts ===
4+
// Issue #62779: Type parameter leak caused by `this` and reverse mapped type
5+
declare function testReverseMapped<T extends Record<string, unknown>>(obj: {
6+
>testReverseMapped : Symbol(testReverseMapped, Decl(reverseMappedThisTypeInference.ts, 0, 0))
7+
>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 1, 35))
8+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
9+
>obj : Symbol(obj, Decl(reverseMappedThisTypeInference.ts, 1, 70))
10+
11+
[K in keyof T]: () => T[K];
12+
>K : Symbol(K, Decl(reverseMappedThisTypeInference.ts, 2, 5))
13+
>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 1, 35))
14+
>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 1, 35))
15+
>K : Symbol(K, Decl(reverseMappedThisTypeInference.ts, 2, 5))
16+
17+
}): T;
18+
>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 1, 35))
19+
20+
const obj = testReverseMapped({
21+
>obj : Symbol(obj, Decl(reverseMappedThisTypeInference.ts, 5, 5))
22+
>testReverseMapped : Symbol(testReverseMapped, Decl(reverseMappedThisTypeInference.ts, 0, 0))
23+
24+
a() {
25+
>a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 5, 31))
26+
27+
return 0;
28+
},
29+
b() {
30+
>b : Symbol(b, Decl(reverseMappedThisTypeInference.ts, 8, 6))
31+
32+
return this.a();
33+
>this.a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 5, 31))
34+
>this : Symbol(__type, Decl(reverseMappedThisTypeInference.ts, 1, 74))
35+
>a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 5, 31))
36+
37+
},
38+
});
39+
40+
// Expected: { a: number; b: number; }
41+
// Actual bug: { a: number; b: T[string]; }
42+
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
//// [tests/cases/compiler/reverseMappedThisTypeInference.ts] ////
2+
3+
=== reverseMappedThisTypeInference.ts ===
4+
// Issue #62779: Type parameter leak caused by `this` and reverse mapped type
5+
declare function testReverseMapped<T extends Record<string, unknown>>(obj: {
6+
>testReverseMapped : <T extends Record<string, unknown>>(obj: { [K in keyof T]: () => T[K]; }) => T
7+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
8+
>obj : { [K in keyof T]: () => T[K]; }
9+
> : ^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^
10+
11+
[K in keyof T]: () => T[K];
12+
}): T;
13+
14+
const obj = testReverseMapped({
15+
>obj : { a: number; b: number; }
16+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
17+
>testReverseMapped({ a() { return 0; }, b() { return this.a(); },}) : { a: number; b: number; }
18+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
19+
>testReverseMapped : <T extends Record<string, unknown>>(obj: { [K in keyof T]: () => T[K]; }) => T
20+
> : ^ ^^^^^^^^^ ^^ ^^ ^^^^^
21+
>{ a() { return 0; }, b() { return this.a(); },} : { a(): number; b(): number; }
22+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23+
24+
a() {
25+
>a : () => number
26+
> : ^^^^^^^^^^^^
27+
28+
return 0;
29+
>0 : 0
30+
> : ^
31+
32+
},
33+
b() {
34+
>b : () => number
35+
> : ^^^^^^^^^^^^
36+
37+
return this.a();
38+
>this.a() : number
39+
> : ^^^^^^
40+
>this.a : () => number
41+
> : ^^^^^^^^^^^^
42+
>this : { a: () => number; b: () => number; }
43+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
44+
>a : () => number
45+
> : ^^^^^^^^^^^^
46+
47+
},
48+
});
49+
50+
// Expected: { a: number; b: number; }
51+
// Actual bug: { a: number; b: T[string]; }
52+
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// @strict: true
2+
3+
// Issue #62779: Type parameter leak caused by `this` and reverse mapped type
4+
declare function testReverseMapped<T extends Record<string, unknown>>(obj: {
5+
[K in keyof T]: () => T[K];
6+
}): T;
7+
8+
const obj = testReverseMapped({
9+
a() {
10+
return 0;
11+
},
12+
b() {
13+
return this.a();
14+
},
15+
});
16+
17+
// Expected: { a: number; b: number; }
18+
// Actual bug: { a: number; b: T[string]; }

0 commit comments

Comments
 (0)