WARNING: THIS SITE IS A MIRROR OF GITHUB.COM / IT CANNOT LOGIN OR REGISTER ACCOUNTS / THE CONTENTS ARE PROVIDED AS-IS / THIS SITE ASSUMES NO RESPONSIBILITY FOR ANY DISPLAYED CONTENT OR LINKS / IF YOU FOUND SOMETHING MAY NOT GOOD FOR EVERYONE, CONTACT ADMIN AT ilovescratch@foxmail.com
Skip to content

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?

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: Not started

Development

Successfully merging this pull request may close these issues.

Type parameter leak caused by this and reverse mapped type

3 participants