🔎 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
⏯ 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.
🔎 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
getDeclarationModifierFlagsFromSymbolExfor synthetic properties", FixgetDeclarationModifierFlagsFromSymbolExfor synthetic properties #63932), found bygit bisectfrom 87e83b7 (good) to 1f70213 (bad), every step built and scored, none skipped.mainat 09b1db0 (2026-10-01).tsc) and@typescript/native-preview7.0.0-dev.20260707.2 accept the code.⏯ Playground Link
No response
💻 Code
🙁 Actual behavior
On
main(09b1db0) and since 8239985:The inferred
Tcontainssecretas a public property, which nokeyof-based mapping could produce.🙂 Expected behavior
No error, as in TypeScript 6.0.3 and earlier native previews.
Tshould be inferred without the private (or protected) members ofAccount, because a reverse mapping recovers the source of a mapped type keyed bykeyof, andkeyofnever yields a private or protected member.Additional information about the issue
Cause. The assertion contextually types the call, so
Tis inferred by reverse-mappingAccountthroughReadonly<Account & T>.resolveReverseMappedTypeMemberscopies every source property,secretincluded. The copy has the member's declarations but no accessibility, so it reads as public.That inferred
Tisn't new; what changed is what it does. Before #63932, the intersection propertysecretinAccount & Tresolved as private,keyofdropped it, andAccountstayed comparable. After #63932, intersection properties take the most permissive accessibility (assyntheticProtectedProperties.tsasserts, 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 Classalone doesn't reproduce it:getAccount<object>()(no inference)Entitywithout a private memberSuggested 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 undertsc6.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.