Skip to content

Commit c2c4e13

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 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.
1 parent 0a07132 commit c2c4e13

File tree

5 files changed

+396
-0
lines changed

5 files changed

+396
-0
lines changed

src/compiler/checker.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31825,6 +31825,13 @@ 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 and the contextual type contains a mapped type (like reverse
31829+
// mapped types used for inference), use the object literal type to allow proper inference
31830+
// of method return types that reference other methods via this. This avoids leaking type
31831+
// parameters like T[K] when methods call each other.
31832+
if (contextualType && someType(contextualType, t => !!(getObjectFlags(t) & ObjectFlags.Mapped)) && getInferenceContext(containingLiteral)) {
31833+
return getWidenedType(checkExpressionCached(containingLiteral));
31834+
}
3182831835
return getWidenedType(contextualType ? getNonNullableType(contextualType) : checkExpressionCached(containingLiteral));
3182931836
}
3183031837
// In an assignment of the form 'obj.xxx = function(...)' or 'obj[xxx] = function(...)', the
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
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+
// Intersection with mapped type
19+
declare function testReverseMapped2<T extends Record<string, unknown>, T2>(
20+
obj: T2 & {
21+
[K in keyof T]: () => T[K];
22+
},
23+
): T;
24+
25+
const obj2 = testReverseMapped2({
26+
a() {
27+
return 0;
28+
},
29+
b() {
30+
return this.a();
31+
},
32+
});
33+
34+
// Union with mapped type
35+
declare function testReverseMapped3<T extends Record<string, unknown>, T2>(
36+
obj: T2 | {
37+
[K in keyof T]: () => T[K];
38+
},
39+
): T;
40+
41+
const obj3 = testReverseMapped3({
42+
a() {
43+
return 0;
44+
},
45+
b() {
46+
return this.a();
47+
},
48+
});
49+
50+
51+
//// [reverseMappedThisTypeInference.js]
52+
"use strict";
53+
var obj = testReverseMapped({
54+
a: function () {
55+
return 0;
56+
},
57+
b: function () {
58+
return this.a();
59+
},
60+
});
61+
var obj2 = testReverseMapped2({
62+
a: function () {
63+
return 0;
64+
},
65+
b: function () {
66+
return this.a();
67+
},
68+
});
69+
var obj3 = testReverseMapped3({
70+
a: function () {
71+
return 0;
72+
},
73+
b: function () {
74+
return this.a();
75+
},
76+
});
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
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+
// Intersection with mapped type
41+
declare function testReverseMapped2<T extends Record<string, unknown>, T2>(
42+
>testReverseMapped2 : Symbol(testReverseMapped2, Decl(reverseMappedThisTypeInference.ts, 12, 3))
43+
>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 15, 36))
44+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
45+
>T2 : Symbol(T2, Decl(reverseMappedThisTypeInference.ts, 15, 70))
46+
47+
obj: T2 & {
48+
>obj : Symbol(obj, Decl(reverseMappedThisTypeInference.ts, 15, 75))
49+
>T2 : Symbol(T2, Decl(reverseMappedThisTypeInference.ts, 15, 70))
50+
51+
[K in keyof T]: () => T[K];
52+
>K : Symbol(K, Decl(reverseMappedThisTypeInference.ts, 17, 9))
53+
>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 15, 36))
54+
>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 15, 36))
55+
>K : Symbol(K, Decl(reverseMappedThisTypeInference.ts, 17, 9))
56+
57+
},
58+
): T;
59+
>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 15, 36))
60+
61+
const obj2 = testReverseMapped2({
62+
>obj2 : Symbol(obj2, Decl(reverseMappedThisTypeInference.ts, 21, 5))
63+
>testReverseMapped2 : Symbol(testReverseMapped2, Decl(reverseMappedThisTypeInference.ts, 12, 3))
64+
65+
a() {
66+
>a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 21, 33))
67+
68+
return 0;
69+
},
70+
b() {
71+
>b : Symbol(b, Decl(reverseMappedThisTypeInference.ts, 24, 6))
72+
73+
return this.a();
74+
>this.a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 21, 33), Decl(reverseMappedThisTypeInference.ts, 21, 33))
75+
>a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 21, 33), Decl(reverseMappedThisTypeInference.ts, 21, 33))
76+
77+
},
78+
});
79+
80+
// Union with mapped type
81+
declare function testReverseMapped3<T extends Record<string, unknown>, T2>(
82+
>testReverseMapped3 : Symbol(testReverseMapped3, Decl(reverseMappedThisTypeInference.ts, 28, 3))
83+
>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 31, 36))
84+
>Record : Symbol(Record, Decl(lib.es5.d.ts, --, --))
85+
>T2 : Symbol(T2, Decl(reverseMappedThisTypeInference.ts, 31, 70))
86+
87+
obj: T2 | {
88+
>obj : Symbol(obj, Decl(reverseMappedThisTypeInference.ts, 31, 75))
89+
>T2 : Symbol(T2, Decl(reverseMappedThisTypeInference.ts, 31, 70))
90+
91+
[K in keyof T]: () => T[K];
92+
>K : Symbol(K, Decl(reverseMappedThisTypeInference.ts, 33, 9))
93+
>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 31, 36))
94+
>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 31, 36))
95+
>K : Symbol(K, Decl(reverseMappedThisTypeInference.ts, 33, 9))
96+
97+
},
98+
): T;
99+
>T : Symbol(T, Decl(reverseMappedThisTypeInference.ts, 31, 36))
100+
101+
const obj3 = testReverseMapped3({
102+
>obj3 : Symbol(obj3, Decl(reverseMappedThisTypeInference.ts, 37, 5))
103+
>testReverseMapped3 : Symbol(testReverseMapped3, Decl(reverseMappedThisTypeInference.ts, 28, 3))
104+
105+
a() {
106+
>a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 37, 33))
107+
108+
return 0;
109+
},
110+
b() {
111+
>b : Symbol(b, Decl(reverseMappedThisTypeInference.ts, 40, 6))
112+
113+
return this.a();
114+
>this.a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 37, 33), Decl(reverseMappedThisTypeInference.ts, 37, 33))
115+
>a : Symbol(a, Decl(reverseMappedThisTypeInference.ts, 37, 33), Decl(reverseMappedThisTypeInference.ts, 37, 33))
116+
117+
},
118+
});
119+
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
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+
// Intersection with mapped type
51+
declare function testReverseMapped2<T extends Record<string, unknown>, T2>(
52+
>testReverseMapped2 : <T extends Record<string, unknown>, T2>(obj: T2 & { [K in keyof T]: () => T[K]; }) => T
53+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^^^^
54+
55+
obj: T2 & {
56+
>obj : T2 & { [K in keyof T]: () => T[K]; }
57+
> : ^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^
58+
59+
[K in keyof T]: () => T[K];
60+
},
61+
): T;
62+
63+
const obj2 = testReverseMapped2({
64+
>obj2 : { a: number; b: number; }
65+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
66+
>testReverseMapped2({ a() { return 0; }, b() { return this.a(); },}) : { a: number; b: number; }
67+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
68+
>testReverseMapped2 : <T extends Record<string, unknown>, T2>(obj: T2 & { [K in keyof T]: () => T[K]; }) => T
69+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^^^^
70+
>{ a() { return 0; }, b() { return this.a(); },} : { a(): number; b(): number; }
71+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
72+
73+
a() {
74+
>a : () => number
75+
> : ^^^^^^^^^^^^
76+
77+
return 0;
78+
>0 : 0
79+
> : ^
80+
81+
},
82+
b() {
83+
>b : () => number
84+
> : ^^^^^^^^^^^^
85+
86+
return this.a();
87+
>this.a() : number
88+
> : ^^^^^^
89+
>this.a : (() => number) & (() => number)
90+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
91+
>this : { a(): number; b(): number; } & { a: () => number; b: () => number; }
92+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
93+
>a : (() => number) & (() => number)
94+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
95+
96+
},
97+
});
98+
99+
// Union with mapped type
100+
declare function testReverseMapped3<T extends Record<string, unknown>, T2>(
101+
>testReverseMapped3 : <T extends Record<string, unknown>, T2>(obj: T2 | { [K in keyof T]: () => T[K]; }) => T
102+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^^^^
103+
104+
obj: T2 | {
105+
>obj : T2 | { [K in keyof T]: () => T[K]; }
106+
> : ^^^^^^^^ ^^^^^^^^^^^^^^^^^^^^ ^^^
107+
108+
[K in keyof T]: () => T[K];
109+
},
110+
): T;
111+
112+
const obj3 = testReverseMapped3({
113+
>obj3 : { a: number; b: number; }
114+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
115+
>testReverseMapped3({ a() { return 0; }, b() { return this.a(); },}) : { a: number; b: number; }
116+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^
117+
>testReverseMapped3 : <T extends Record<string, unknown>, T2>(obj: T2 | { [K in keyof T]: () => T[K]; }) => T
118+
> : ^ ^^^^^^^^^ ^^ ^^ ^^ ^^^^^
119+
>{ a() { return 0; }, b() { return this.a(); },} : { a(): number; b(): number; }
120+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
121+
122+
a() {
123+
>a : () => number
124+
> : ^^^^^^^^^^^^
125+
126+
return 0;
127+
>0 : 0
128+
> : ^
129+
130+
},
131+
b() {
132+
>b : () => number
133+
> : ^^^^^^^^^^^^
134+
135+
return this.a();
136+
>this.a() : number
137+
> : ^^^^^^
138+
>this.a : (() => number) | (() => number)
139+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
140+
>this : { a(): number; b(): number; } | { a: () => number; b: () => number; }
141+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
142+
>a : (() => number) | (() => number)
143+
> : ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
144+
145+
},
146+
});
147+

0 commit comments

Comments
 (0)