Skip to content

Commit 24ba7eb

Browse files
Fix #13948: preserve narrow types for computed property keys with union literal types
When the computed property name type in an object literal is a union of literal property name types (e.g., `'a' | 'b'`), distribute the property over the union members, creating a separate named property for each constituent type. Before this fix, `{ [key]: value }` where `key: 'a' | 'b'` would produce `{ [x: string]: V }` because `isTypeUsableAsPropertyName` rejects union types. Now it produces `{ a: V; b: V }`, consistent with how mapped types handle the same scenario. This fixes the long-standing React setState pattern: ```ts this.setState({ [key]: value }); // no longer errors ``` Co-authored-by: Cursor <cursoragent@cursor.com>
1 parent 87aa917 commit 24ba7eb

File tree

7 files changed

+669
-6
lines changed

7 files changed

+669
-6
lines changed

src/compiler/checker.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33581,6 +33581,33 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3358133581
}
3358233582
}
3358333583
objectFlags |= getObjectFlags(type) & ObjectFlags.PropagatingFlags;
33584+
33585+
// When the computed property name type is a union of literal property name types,
33586+
// distribute the property over the union members, creating a separate named property
33587+
// for each. This fixes #13948 where { [key]: value } with key: 'a' | 'b' would
33588+
// produce { [x: string]: V } instead of { a: V; b: V }.
33589+
if (
33590+
computedNameType && (computedNameType.flags & TypeFlags.Union) &&
33591+
every((computedNameType as UnionType).types, isTypeUsableAsPropertyName)
33592+
) {
33593+
for (const constituentType of (computedNameType as UnionType).types) {
33594+
const propName = getPropertyNameFromType(constituentType as StringLiteralType | NumberLiteralType | UniqueESSymbolType);
33595+
const distributedProp = createSymbol(SymbolFlags.Property | member.flags, propName, checkFlags | CheckFlags.Late);
33596+
distributedProp.links.nameType = constituentType;
33597+
distributedProp.declarations = member.declarations;
33598+
distributedProp.parent = member.parent;
33599+
if (member.valueDeclaration) {
33600+
distributedProp.valueDeclaration = member.valueDeclaration;
33601+
}
33602+
distributedProp.links.type = type;
33603+
distributedProp.links.target = member;
33604+
propertiesTable.set(distributedProp.escapedName, distributedProp);
33605+
propertiesArray.push(distributedProp);
33606+
allPropertiesTable?.set(distributedProp.escapedName, distributedProp);
33607+
}
33608+
continue;
33609+
}
33610+
3358433611
const nameType = computedNameType && isTypeUsableAsPropertyName(computedNameType) ? computedNameType : undefined;
3358533612
const prop = nameType ?
3358633613
createSymbol(SymbolFlags.Property | member.flags, getPropertyNameFromType(nameType), checkFlags | CheckFlags.Late) :
Lines changed: 216 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,216 @@
1+
//// [tests/cases/compiler/computedPropertyNamesUnionTypes.ts] ////
2+
3+
=== computedPropertyNamesUnionTypes.ts ===
4+
// Fixes #13948: Computed property key names should not be widened when the key
5+
// type is a union of literal types.
6+
7+
interface Person {
8+
>Person : Symbol(Person, Decl(computedPropertyNamesUnionTypes.ts, 0, 0))
9+
10+
name: string;
11+
>name : Symbol(Person.name, Decl(computedPropertyNamesUnionTypes.ts, 3, 18))
12+
13+
age: number;
14+
>age : Symbol(Person.age, Decl(computedPropertyNamesUnionTypes.ts, 4, 17))
15+
}
16+
17+
// Union of string literal keys should produce distributed named properties
18+
function unionStringLiterals(key: 'a' | 'b', value: number) {
19+
>unionStringLiterals : Symbol(unionStringLiterals, Decl(computedPropertyNamesUnionTypes.ts, 6, 1))
20+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 9, 29))
21+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 9, 44))
22+
23+
const obj = { [key]: value };
24+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 10, 9))
25+
>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 10, 17))
26+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 9, 29))
27+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 9, 44))
28+
29+
obj.a; // ok
30+
>obj.a : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 10, 17))
31+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 10, 9))
32+
>a : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 10, 17))
33+
34+
obj.b; // ok
35+
>obj.b : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 10, 17))
36+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 10, 9))
37+
>b : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 10, 17))
38+
}
39+
40+
// keyof should work with computed properties
41+
function keyofComputed(key: keyof Person, value: string | number) {
42+
>keyofComputed : Symbol(keyofComputed, Decl(computedPropertyNamesUnionTypes.ts, 13, 1))
43+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 16, 23))
44+
>Person : Symbol(Person, Decl(computedPropertyNamesUnionTypes.ts, 0, 0))
45+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 16, 41))
46+
47+
const obj = { [key]: value };
48+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 17, 9))
49+
>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 17, 17))
50+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 16, 23))
51+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 16, 41))
52+
53+
obj.name; // ok
54+
>obj.name : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 17, 17))
55+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 17, 9))
56+
>name : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 17, 17))
57+
58+
obj.age; // ok
59+
>obj.age : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 17, 17))
60+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 17, 9))
61+
>age : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 17, 17))
62+
}
63+
64+
// Partial<T> assignability (React setState pattern)
65+
declare function setState(state: Partial<Person>): void;
66+
>setState : Symbol(setState, Decl(computedPropertyNamesUnionTypes.ts, 20, 1))
67+
>state : Symbol(state, Decl(computedPropertyNamesUnionTypes.ts, 23, 26))
68+
>Partial : Symbol(Partial, Decl(lib.es5.d.ts, --, --))
69+
>Person : Symbol(Person, Decl(computedPropertyNamesUnionTypes.ts, 0, 0))
70+
71+
function reactSetState(key: 'name', value: string) {
72+
>reactSetState : Symbol(reactSetState, Decl(computedPropertyNamesUnionTypes.ts, 23, 56))
73+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 24, 23))
74+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 24, 35))
75+
76+
setState({ [key]: value }); // should not error
77+
>setState : Symbol(setState, Decl(computedPropertyNamesUnionTypes.ts, 20, 1))
78+
>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 25, 14))
79+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 24, 23))
80+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 24, 35))
81+
}
82+
83+
// Three-member union
84+
function threeWay(key: 'x' | 'y' | 'z', value: boolean) {
85+
>threeWay : Symbol(threeWay, Decl(computedPropertyNamesUnionTypes.ts, 26, 1))
86+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 29, 18))
87+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 29, 39))
88+
89+
const obj = { [key]: value };
90+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 30, 9))
91+
>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17))
92+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 29, 18))
93+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 29, 39))
94+
95+
obj.x; // ok
96+
>obj.x : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17))
97+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 30, 9))
98+
>x : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17))
99+
100+
obj.y; // ok
101+
>obj.y : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17))
102+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 30, 9))
103+
>y : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17))
104+
105+
obj.z; // ok
106+
>obj.z : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17))
107+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 30, 9))
108+
>z : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 30, 17))
109+
}
110+
111+
// Number literal union
112+
function numberLiterals(key: 0 | 1, value: string) {
113+
>numberLiterals : Symbol(numberLiterals, Decl(computedPropertyNamesUnionTypes.ts, 34, 1))
114+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 37, 24))
115+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 37, 35))
116+
117+
const obj = { [key]: value };
118+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 38, 9))
119+
>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 38, 17))
120+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 37, 24))
121+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 37, 35))
122+
}
123+
124+
// Union key + fixed properties
125+
function withFixed(key: 'a' | 'b') {
126+
>withFixed : Symbol(withFixed, Decl(computedPropertyNamesUnionTypes.ts, 39, 1))
127+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 42, 19))
128+
129+
const obj = { [key]: 1, fixed: 'hello' };
130+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 43, 9))
131+
>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 43, 17))
132+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 42, 19))
133+
>fixed : Symbol(fixed, Decl(computedPropertyNamesUnionTypes.ts, 43, 27))
134+
135+
obj.a; // ok, number
136+
>obj.a : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 43, 17))
137+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 43, 9))
138+
>a : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 43, 17))
139+
140+
obj.b; // ok, number
141+
>obj.b : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 43, 17))
142+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 43, 9))
143+
>b : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 43, 17))
144+
145+
obj.fixed; // ok, string
146+
>obj.fixed : Symbol(fixed, Decl(computedPropertyNamesUnionTypes.ts, 43, 27))
147+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 43, 9))
148+
>fixed : Symbol(fixed, Decl(computedPropertyNamesUnionTypes.ts, 43, 27))
149+
}
150+
151+
// Mapped type equivalence
152+
type Mapped = { [P in 'x' | 'y']: boolean };
153+
>Mapped : Symbol(Mapped, Decl(computedPropertyNamesUnionTypes.ts, 47, 1))
154+
>P : Symbol(P, Decl(computedPropertyNamesUnionTypes.ts, 50, 17))
155+
156+
function mappedEquivalence(key: 'x' | 'y', value: boolean) {
157+
>mappedEquivalence : Symbol(mappedEquivalence, Decl(computedPropertyNamesUnionTypes.ts, 50, 44))
158+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 51, 27))
159+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 51, 42))
160+
161+
const obj = { [key]: value };
162+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 52, 9))
163+
>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 52, 17))
164+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 51, 27))
165+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 51, 42))
166+
167+
const mapped: Mapped = obj; // should be assignable
168+
>mapped : Symbol(mapped, Decl(computedPropertyNamesUnionTypes.ts, 53, 9))
169+
>Mapped : Symbol(Mapped, Decl(computedPropertyNamesUnionTypes.ts, 47, 1))
170+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 52, 9))
171+
}
172+
173+
// Non-literal key should still produce index signature (unchanged behavior)
174+
function dynamicKey(key: string, value: number) {
175+
>dynamicKey : Symbol(dynamicKey, Decl(computedPropertyNamesUnionTypes.ts, 54, 1))
176+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 57, 20))
177+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 57, 32))
178+
179+
const obj = { [key]: value };
180+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 58, 9))
181+
>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 58, 17))
182+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 57, 20))
183+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 57, 32))
184+
185+
obj.anything; // ok via index signature
186+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 58, 9))
187+
}
188+
189+
// Template literal key should still produce index signature
190+
function templateKey(key: `prefix_${string}`, value: number) {
191+
>templateKey : Symbol(templateKey, Decl(computedPropertyNamesUnionTypes.ts, 60, 1))
192+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 63, 21))
193+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 63, 45))
194+
195+
const obj = { [key]: value };
196+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 64, 9))
197+
>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 64, 17))
198+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 63, 21))
199+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 63, 45))
200+
}
201+
202+
// Generic extends literal union should work
203+
function genericKey<K extends 'a' | 'b'>(key: K, value: number) {
204+
>genericKey : Symbol(genericKey, Decl(computedPropertyNamesUnionTypes.ts, 65, 1))
205+
>K : Symbol(K, Decl(computedPropertyNamesUnionTypes.ts, 68, 20))
206+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 68, 41))
207+
>K : Symbol(K, Decl(computedPropertyNamesUnionTypes.ts, 68, 20))
208+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 68, 48))
209+
210+
const obj = { [key]: value };
211+
>obj : Symbol(obj, Decl(computedPropertyNamesUnionTypes.ts, 69, 9))
212+
>[key] : Symbol([key], Decl(computedPropertyNamesUnionTypes.ts, 69, 17))
213+
>key : Symbol(key, Decl(computedPropertyNamesUnionTypes.ts, 68, 41))
214+
>value : Symbol(value, Decl(computedPropertyNamesUnionTypes.ts, 68, 48))
215+
}
216+

0 commit comments

Comments
 (0)