Lift computed property union literal types to union of object types#63120
Open
DukeDeSouth wants to merge 1 commit intomicrosoft:mainfrom
Open
Lift computed property union literal types to union of object types#63120DukeDeSouth wants to merge 1 commit intomicrosoft:mainfrom
DukeDeSouth wants to merge 1 commit intomicrosoft:mainfrom
Conversation
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 overly
wide, and the unsound alternative ({ a: V; b: V }) was correctly rejected.
Fixes microsoft#13948
Prior art: microsoft#21070 by @sandersn (2018) implemented the same union-type approach
but was shelved due to baseline complexity. This implementation adapts the core
idea 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" expression
Co-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
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
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.
Human View
Summary
When a computed property name has a union literal type, the inferred object literal type is now a union of object types instead of a string/number index signature.
This is sound because at runtime
{ [key]: value }creates exactly one 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 to fix #13948 but produced
{ a: number; b: number }(intersection-like), which is unsound — 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 M7 Cursor