Repository navigation
Reverse-mapped inference exposes private members as public; since #63932 this rejects f<T>() as C #64593
Description
🔎 Search Terms
reverse mapped type private member, resolveReverseMappedTypeMembers, private is private in type but not in type, TS2352 private intersection readonly, inference private property public, keyof private member, syntheticProtectedProperties, 63932
🕗 Version & Regression Information
- This changed in commit or PR 8239985 ("Fix
getDeclarationModifierFlagsFromSymbolExfor synthetic properties", FixgetDeclarationModifierFlagsFromSymbolExfor synthetic properties #63932), found bygit bisectfrom 87e83b7 (good) to 1f70213 (bad), every step built and scored, none skipped. - Still present on
mainat 09b1db0 (2026-10-01). - TypeScript 6.0.3 (
tsc) and@typescript/native-preview7.0.0-dev.20260707.2 accept the code.
⏯ Playground Link
No response
💻 Code
// @strict: true
class Entity {
private secret = 1;
markChanged(field: string): void {}
}
class Account extends Entity {
emailAddress = '';
}
declare function getAccount<T extends object = object>(): Readonly<Account & T>;
export const ok = getAccount<object>() as Account; // fine everywhere
export const b = getAccount() as Account; // TS2352 since 8239985a01🙁 Actual behavior
On main (09b1db0) and since 8239985:
error TS2352: Conversion of type 'Readonly<Account & { secret: number; markChanged: (field: string) => void; emailAddress: string; }>' to type 'Account' may be a mistake because neither type sufficiently overlaps with the other.
Property 'secret' is private in type 'Account' but not in type 'Readonly<Account & { secret: number; ... }>'.
The inferred T contains secret as a public property, which no keyof-based mapping could produce.
🙂 Expected behavior
No error, as in TypeScript 6.0.3 and earlier native previews. T should be inferred without the private (or protected) members of Account, because a reverse mapping recovers the source of a mapped type keyed by keyof, and keyof never yields a private or protected member.
Additional information about the issue
Cause. The assertion contextually types the call, so T is inferred by reverse-mapping Account through Readonly<Account & T>. resolveReverseMappedTypeMembers copies every source property, secret included. The copy has the member's declarations but no accessibility, so it reads as public.
That inferred T isn't new; what changed is what it does. Before #63932, the intersection property secret in Account & T resolved as private, keyof dropped it, and Account stayed comparable. After #63932, intersection properties take the most permissive accessibility (as syntheticProtectedProperties.ts asserts, deliberately), so the public copy wins and the comparison fails both ways. So #63932 isn't the bug; it exposed one.
Both conditions are required, which is why Readonly<Class & object> as Class alone doesn't reproduce it:
| variant | result |
|---|---|
explicit getAccount<object>() (no inference) |
no error |
Entity without a private member |
no error |
| inference + private member | TS2352 |
Suggested fix. Skip non-public source properties in resolveReverseMappedTypeMembers. That leaves #63932's accessibility rule untouched. I have a PR ready with a regression test and baselines, and the full pre-PR checklist passing.
Real-world impact. Hit in a real codebase on two casts of the shape context.getAccount() as Account, where an entity base class carries private members. The code is correct and type-checks under tsc 6.0.3.
AI disclosure. I found and diagnosed this with the help of Claude Code (Anthropic). I've read and understand the analysis, and I'll be the one following up here.
Activity
gabritto commentedon Oct 27, 2026
There's a simpler repro that precedes #63932: