Skip to content

Fix type parameter leak when using 'this' in reverse mapped types#62931

Closed
thromel wants to merge 1 commit intomicrosoft:mainfrom
thromel:fix/reverse-mapped-this-type-leak
Closed

Fix type parameter leak when using 'this' in reverse mapped types#62931
thromel wants to merge 1 commit intomicrosoft:mainfrom
thromel:fix/reverse-mapped-this-type-leak

Conversation

@thromel
Copy link

@thromel thromel commented Dec 28, 2025

Fixes #62779

Problem

When object literal methods reference this inside a function with a reverse mapped type parameter, the type parameter T was leaking through:

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(); },
});
// Expected: { a: number; b: number; }
// Actual:   { a: number; b: T[string]; } (widened to unknown)

Cause

In getContextualThisParameterType, when computing the this type for method b, the code was using the contextual type (the mapped type { [K in keyof T]: () => T[K] }) with unresolved type parameters. This caused this.a() to be typed as T["a"] rather than the already-inferred number.

Solution

When in an inference context, use the actual object literal type via checkExpressionCached(containingLiteral) rather than the contextual mapped type. This allows methods to see each other's already-inferred types when resolving this references.

Test Plan

  • Added test case reverseMappedThisTypeInference.ts demonstrating the fix
  • Verified all existing contextual, inference, and reverse mapped type tests pass

@github-project-automation github-project-automation bot moved this to Not started in PR Backlog Dec 28, 2025
@typescript-bot typescript-bot added the For Backlog Bug PRs that fix a backlog bug label Dec 28, 2025
@thromel thromel force-pushed the fix/reverse-mapped-this-type-leak branch from 602ed22 to 7155d8f Compare December 28, 2025 06:45
// @strict: true

// Issue #62779: Type parameter leak caused by `this` and reverse mapped type
declare function testReverseMapped<T extends Record<string, unknown>>(obj: {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

declare function testReverseMapped2<T extends Record<string, unknown>, T2>(
  obj: T2 & {
    [K in keyof T]: () => T[K];
  },
): T;

const obj2 = testReverseMapped2({
  a() {
    return 0;
  },
  b() {
    return this.a();
  },
});

declare function testReverseMapped3<T extends Record<string, unknown>, T2>(
  obj: T2 | {
    [K in keyof T]: () => T[K];
  },
): T;

const obj3 = testReverseMapped3({
  a() {
    return 0;
  },
  b() {
    // Property 'a' does not exist on type '{} | { [K in keyof T]: () => T[K]; }'.
    //   Property 'a' does not exist on type '{}'.(2339)
    return this.a();
  },
});

IMHO, at the very least - the type contained in this error message indicates the fix presented in this PR isn't quite right.

Fixes microsoft#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.
@thromel thromel force-pushed the fix/reverse-mapped-this-type-leak branch from 7155d8f to c2c4e13 Compare December 30, 2025 10:38
@thromel
Copy link
Author

thromel commented Dec 30, 2025

Thanks for the feedback! I've updated the fix to use someType to check if the contextual type contains a mapped type (not just IS a mapped type), which should handle both intersection and union cases.

With this change, both test cases now work:

  • obj2 (intersection): { a: number; b: number; }
  • obj3 (union): { a: number; b: number; }

Is this the behavior you were expecting, or did you have a different approach in mind?

@typescript-bot
Copy link
Collaborator

With 6.0 out as the final release vehicle for this codebase, we're closing all PRs that don't fit the merge criteria for post-6.0 patches. If you think this was a mistake and this PR fits the post-6.0 patch criteria, please post to the 6.0 iteration issue with details (specifically, which PR and which patch criteria it satisfies).

Next steps for PRs:

  • For crash bugfixes or language service improvements, PRs are currently accepted at the typescript-go repo
  • Changes to type system behavior should wait until after 7.0, at which point mainline TypeScript development will resume in this repository with the Go codebase
  • Library file updates (lib.d.ts etc) continue to live in this repo or the DOM Generator repo as appropriate

@github-project-automation github-project-automation bot moved this from Not started to Done in PR Backlog Mar 24, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

For Backlog Bug PRs that fix a backlog bug

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

Type parameter leak caused by this and reverse mapped type

3 participants