You signed in with another tab or window.Reload to refresh your session.You signed out in another tab or window.Reload to refresh your session.You switched accounts on another tab or window.Reload to refresh your session.Dismiss alert
When a computed property name has a union literal type, the inferred object literal type is now aunion of object types instead of a string/number index signature.
declarevarkey:'a'|'b';constobj={[key]:1};// Before: { [x: string]: number } (overly wide index signature)// After: { a: number } | { b: number } (precise union — sound)
This is sound because at runtime{ [key]: value } creates exactlyone property, not all possible properties. The type system should reflect this: the result is one of N possible objects, not an object with all N properties.
Motivation
Issue#13948 (open since 2017): computed property names with union literal keys produce an index signature, losing type precision. Users expected{ a: number } | { b: number } but got{ [x: string]: number }.
Prior Art & Transparency
An earlier PR from this author (#63113) attempted tofix#13948 but produced{ a: number; b: number } (intersection-like), which isunsound — at runtime only one property exists, so accessing both.a and.b is guaranteed to fail on one. That PR was closed after the soundness issue was identified by@mkantor.
@sandersn's PR#21070 (2018) implemented the correct union-type approach:
o has the type{ a: string } | { b: string }
That PR was shelved due to complexity (baseline changes, destructuring interactions), not because the approach was wrong. This implementation adapts the core idea to the current checker architecture.
What Changed
src/compiler/checker.ts —checkObjectLiteral:
When processing aPropertyAssignment with a computed name whose type is a union of literal types (checked viaisTypeUsableAsPropertyName on each union member):
Flush accumulated properties into the intermediate spread type
Create one object type per union member, each with a single named property
Union all member types together
Spread the union with the intermediate type (leveraging the existing spread-distributes-over-unions mechanism)
This naturally produces cross-product unions for multiple union computed properties:
Single literal keys ('a', not a union): unchanged behavior
Non-literal computed keys (string,number): still produce index signatures
Generic type parameters (K extends 'a' | 'b'): not resolved unions, unchanged
Mapped types ({ [K in 'a' | 'b']: V }): still produce{ a: V; b: V } as designed
Record types,Partial,Pick: completely unaffected
Spread mechanics: existing behavior preserved
Class and interface computed properties: unaffected (only object literals)
Soundness Verification
Verified with type-level assertions:
typeEquals<X,Y>=(<T>()=>TextendsX ?1 :2)extends(<T>()=>TextendsY ?1 :2) ?true :false;declarefunctionassert<Textendstrue>():void;declarevarab:'a'|'b';constobj={[ab]:1};// Exact type checkassert<Equals<typeofobj,{a:number}|{b:number}>>();// pass// NOT an intersection (the unsound alternative)typeNotIntersection=typeofobjextends{a:number;b:number} ?'UNSOUND' :'SOUND';assert<Equals<NotIntersection,'SOUND'>>();// pass// Mapped types unaffectedassert<Equals<{[Kin'a'|'b']:number},{a:number;b:number}>>();// pass
Adversarial Testing (12 scenarios, 106,326 tests)
We ran extensive adversarial testing to avoid whack-a-mole regressions:
Scenario
Result
Full compiler test suite (43,608 tests)
Pass
Full conformance test suite (46,188 tests)
Pass
Full test suite (106,326 tests)
Pass
React setState pattern
Sound
Contextual typing (assignment, return, args)
Correct
Destructuring with union keys
Correct
as const + union computed
Correct
Excess property checks
Correct
Generic K (must NOT trigger lifting)
Not triggered
Spread combinations (before/after/overlap)
Correct
Declaration emit
Correct union types
Real-world patterns (Redux, CSS-in-JS, config)
All pass
Baseline Changes
Two existing baselines updated (expected — our fix produces more precise types):
declarationEmitSimpleComputedNames1:Math.random() > 0.5 ? "f1" : "f2" computed property now produces{ f1(): string } | { f2(): string } instead of retained computed property name
declarationComputedPropertyNames (transpile): same pattern — union type instead of index signatures
Known Limitation
When mixing non-literal computed properties (producing index signatures) with union literal computed properties in the same object literal, the index signatures from the non-literal properties may not be preserved in the final type. This is consistent with howgetSpreadType handles index signatures generally (they are dropped when one side of the spread lacks them). This is a very rare pattern and the result is strictly more conservative (never unsound).
A Note on Process
This is a second attempt at#13948. The first attempt (#63113) was fundamentally flawed — it confused mapped type semantics ({ [P in K]: V } iterates all keys) with computed property runtime semantics ({ [key]: V } picks one key). That PR was closed after a soundness issue was correctly identified.
This time we studied@sandersn's correct approach from#21070, ran 12 adversarial test scenarios beyond the standard test suite, verified exact types with type-level assertions, and checked that generics/mapped types/Record/Partial are completely unaffected. We have done our best to be thorough, but if we have missed an edge case — we sincerely apologize and will address it immediately.
When a computed property name has a union literal type (e.g., key: 'a' | 'b'),the resulting object literal type is now a union of object types({ a: V } | { b: V }) instead of an index signature ({ [x: string]: V }).This is sound because at runtime { [key]: value } creates exactly one property,not all possible properties. The previous behavior (index signature) was overlywide, and the unsound alternative ({ a: V; b: V }) was correctly rejected.Fixesmicrosoft#13948Prior art:microsoft#21070 by@sandersn (2018) implemented the same union-type approachbut was shelved due to baseline complexity. This implementation adapts the coreidea to the modern checker architecture.Baseline changes:- declarationEmitSimpleComputedNames1: union literal computed properties now produce { f1 } | { f2 } instead of retained computed property name- declarationComputedPropertyNames (transpile): same — union instead of index signature for Math.random() > 0.5 ? "f1" : "f2" expressionCo-authored-by: Cursor <cursoragent@cursor.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.Learn more about bidirectional Unicode characters
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading.Please reload this page.
Human View
Summary
When a computed property name has a union literal type, the inferred object literal type is now aunion of object types instead of a string/number index signature.
This is sound because at runtime
{ [key]: value }creates exactlyone property, not all possible properties. The type system should reflect this: the result is one of N possible objects, not an object with all N properties.Motivation
Issue#13948 (open since 2017): computed property names with union literal keys produce an index signature, losing type precision. Users expected
{ a: number } | { b: number }but got{ [x: string]: number }.Prior Art & Transparency
An earlier PR from this author (#63113) attempted tofix#13948 but produced
{ a: number; b: number }(intersection-like), which isunsound — at runtime only one property exists, so accessing both.aand.bis guaranteed to fail on one. That PR was closed after the soundness issue was identified by@mkantor.@sandersn's PR#21070 (2018) implemented the correct union-type approach:
That PR was shelved due to complexity (baseline changes, destructuring interactions), not because the approach was wrong. This implementation adapts the core idea to the current checker architecture.
What Changed
src/compiler/checker.ts—checkObjectLiteral:When processing a
PropertyAssignmentwith a computed name whose type is a union of literal types (checked viaisTypeUsableAsPropertyNameon each union member):This naturally produces cross-product unions for multiple union computed properties:
What Does NOT Change
'a', not a union): unchanged behaviorstring,number): still produce index signaturesK extends 'a' | 'b'): not resolved unions, unchanged{ [K in 'a' | 'b']: V }): still produce{ a: V; b: V }as designedSoundness Verification
Verified with type-level assertions:
Adversarial Testing (12 scenarios, 106,326 tests)
We ran extensive adversarial testing to avoid whack-a-mole regressions:
as const+ union computedBaseline Changes
Two existing baselines updated (expected — our fix produces more precise types):
declarationEmitSimpleComputedNames1:Math.random() > 0.5 ? "f1" : "f2"computed property now produces{ f1(): string } | { f2(): string }instead of retained computed property namedeclarationComputedPropertyNames(transpile): same pattern — union type instead of index signaturesKnown Limitation
When mixing non-literal computed properties (producing index signatures) with union literal computed properties in the same object literal, the index signatures from the non-literal properties may not be preserved in the final type. This is consistent with how
getSpreadTypehandles index signatures generally (they are dropped when one side of the spread lacks them). This is a very rare pattern and the result is strictly more conservative (never unsound).A Note on Process
This is a second attempt at#13948. The first attempt (#63113) was fundamentally flawed — it confused mapped type semantics (
{ [P in K]: V }iterates all keys) with computed property runtime semantics ({ [key]: V }picks one key). That PR was closed after a soundness issue was correctly identified.This time we studied@sandersn's correct approach from#21070, ran 12 adversarial test scenarios beyond the standard test suite, verified exact types with type-level assertions, and checked that generics/mapped types/Record/Partial are completely unaffected. We have done our best to be thorough, but if we have missed an edge case — we sincerely apologize and will address it immediately.
Fixes#13948
AI View (DCCE Protocol v1.0)
Metadata
AI Contribution Summary
checkObjectLiteral(~46 lines added to checker.ts)Verification Steps
Human Review Guidance
computedNameType.flags & TypeFlags.Unioncorrectly identifies only resolved union types and not generic type parametersMade with M7Cursor