Movatterモバイル変換


[0]ホーム

URL:


Skip to content

Navigation Menu

Sign in
Appearance settings

Search code, repositories, users, issues, pull requests...

Provide feedback

We read every piece of feedback, and take your input very seriously.

Saved searches

Use saved searches to filter your results more quickly

Sign up
Appearance settings

Comments

Lift computed property union literal types to union of object types#63120

Open
DukeDeSouth wants to merge 1 commit intomicrosoft:mainfrom
DukeDeSouth:fix/computed-property-union-type-lift
Open

Lift computed property union literal types to union of object types#63120
DukeDeSouth wants to merge 1 commit intomicrosoft:mainfrom
DukeDeSouth:fix/computed-property-union-type-lift

Conversation

@DukeDeSouth
Copy link

@DukeDeSouthDukeDeSouth commentedFeb 8, 2026
edited
Loading

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.

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.tscheckObjectLiteral:

When processing aPropertyAssignment with a computed name whose type is a union of literal types (checked viaisTypeUsableAsPropertyName on each union member):

  1. Flush accumulated properties into the intermediate spread type
  2. Create one object type per union member, each with a single named property
  3. Union all member types together
  4. 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:

declarevarab:'a'|'b';declarevarcd:'c'|'d';constobj={[ab]:'hi',m:1,[cd]:'there'};// Type: { a: string; m: number; c: string }//     | { a: string; m: number; d: string }//     | { b: string; m: number; c: string }//     | { b: string; m: number; d: string }

What Does NOT Change

  • 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:

ScenarioResult
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 patternSound
Contextual typing (assignment, return, args)Correct
Destructuring with union keysCorrect
as const + union computedCorrect
Excess property checksCorrect
Generic K (must NOT trigger lifting)Not triggered
Spread combinations (before/after/overlap)Correct
Declaration emitCorrect 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):

  1. declarationEmitSimpleComputedNames1:Math.random() > 0.5 ? "f1" : "f2" computed property now produces{ f1(): string } | { f2(): string } instead of retained computed property name
  2. 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.

Fixes#13948


AI View (DCCE Protocol v1.0)

Metadata

  • AI Tool: Cursor (Claude claude-4.6-opus)
  • Contribution Type: Bug fix (soundness improvement)
  • Confidence Level: High — 106,326 tests pass, 12 adversarial scenarios verified
  • Prior Art:@sandersn PRComputed property union lifting #21070 (2018), adapted to modern checker API

AI Contribution Summary

  • Studied 1,763-line diff from@sandersn'sComputed property union lifting #21070 to understand correct union-type approach
  • Implemented union type lifting incheckObjectLiteral (~46 lines added to checker.ts)
  • Created comprehensive test file with union, cross-product, spread, number, and enum scenarios
  • Ran 12 adversarial test categories including type-level exact assertions
  • Documented known limitation (index sig preservation in mixed patterns)

Verification Steps

  • Full TypeScript test suite: 106,326 passing, 0 failures
  • Type-level soundness assertions (Equals type)
  • Generic K does NOT trigger union lifting
  • Mapped types / Record / Partial unaffected
  • Declaration emit produces correct union types
  • Real-world patterns (React setState, Redux, CSS-in-JS) verified

Human Review Guidance

  • Critical check: Verify thatcomputedNameType.flags & TypeFlags.Union correctly identifies only resolved union types and not generic type parameters
  • Edge case: Index signature preservation when mixing non-literal + union literal computed properties (documented as known limitation)
  • Baseline review: Two updated baselines produce strictly more precise types

Made with M7Cursor

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>
@github-project-automationgithub-project-automationbot moved this toNot started inPR BacklogFeb 8, 2026
@typescript-bottypescript-bot added the For Backlog BugPRs that fix a backlog bug labelFeb 8, 2026
Sign up for freeto join this conversation on GitHub. Already have an account?Sign in to comment

Reviewers

No reviews

Assignees

No one assigned

Labels

For Backlog BugPRs that fix a backlog bug

Projects

Status: Not started

Milestone

No milestone

Development

Successfully merging this pull request may close these issues.

Computed property key names should not be widened

2 participants

@DukeDeSouth@typescript-bot

[8]ページ先頭

©2009-2026 Movatter.jp