Skip to content

Commit 83ebbc3

Browse files
Lift computed property union literal types to union of object types
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 #13948 Prior art: #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>
1 parent 87aa917 commit 83ebbc3

File tree

8 files changed

+551
-7
lines changed

8 files changed

+551
-7
lines changed

src/compiler/checker.ts

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33581,6 +33581,52 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker {
3358133581
}
3358233582
}
3358333583
objectFlags |= getObjectFlags(type) & ObjectFlags.PropagatingFlags;
33584+
33585+
// When a computed property name has a union literal type (e.g., key: 'a' | 'b'),
33586+
// lift to a union of object types: { a: V } | { b: V }
33587+
// This is sound because at runtime { [key]: value } creates exactly ONE property.
33588+
// See: https://github.com/microsoft/TypeScript/issues/13948
33589+
if (
33590+
computedNameType &&
33591+
(computedNameType.flags & TypeFlags.Union) &&
33592+
every((computedNameType as UnionType).types, isTypeUsableAsPropertyName)
33593+
) {
33594+
// Flush any accumulated properties into the spread
33595+
if (propertiesArray.length > 0) {
33596+
spread = getSpreadType(spread, createObjectLiteralType(), node.symbol, objectFlags, inConstContext);
33597+
propertiesArray = [];
33598+
propertiesTable = createSymbolTable();
33599+
hasComputedStringProperty = false;
33600+
hasComputedNumberProperty = false;
33601+
hasComputedSymbolProperty = false;
33602+
}
33603+
// Create one object type per union member, then union them
33604+
const memberTypes: Type[] = [];
33605+
for (const literalType of (computedNameType as UnionType).types) {
33606+
const propName = getPropertyNameFromType(literalType as StringLiteralType | NumberLiteralType | UniqueESSymbolType);
33607+
const prop = createSymbol(SymbolFlags.Property | member.flags, propName, checkFlags | CheckFlags.Late);
33608+
prop.links.nameType = literalType;
33609+
prop.declarations = member.declarations;
33610+
prop.parent = member.parent;
33611+
if (member.valueDeclaration) {
33612+
prop.valueDeclaration = member.valueDeclaration;
33613+
}
33614+
prop.links.type = type;
33615+
prop.links.target = member;
33616+
33617+
const singlePropTable = createSymbolTable();
33618+
singlePropTable.set(propName, prop);
33619+
const singleObjType = createAnonymousType(node.symbol, singlePropTable, emptyArray, emptyArray, emptyArray);
33620+
singleObjType.objectFlags |= objectFlags | ObjectFlags.ObjectLiteral | ObjectFlags.ContainsObjectOrArrayLiteral;
33621+
memberTypes.push(singleObjType);
33622+
}
33623+
if (memberTypes.length > 0) {
33624+
spread = getSpreadType(spread, getUnionType(memberTypes), node.symbol, objectFlags, inConstContext);
33625+
}
33626+
offset = propertiesArray.length;
33627+
continue;
33628+
}
33629+
3358433630
const nameType = computedNameType && isTypeUsableAsPropertyName(computedNameType) ? computedNameType : undefined;
3358533631
const prop = nameType ?
3358633632
createSymbol(SymbolFlags.Property | member.flags, getPropertyNameFromType(nameType), checkFlags | CheckFlags.Late) :
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
//// [tests/cases/conformance/es6/computedProperties/computedPropertyUnionLiftsToUnionType.ts] ////
2+
3+
//// [computedPropertyUnionLiftsToUnionType.ts]
4+
declare var ab: 'a' | 'b';
5+
declare var cd: 'c' | 'd';
6+
declare var onetwo: 1 | 2;
7+
enum Alphabet {
8+
Aleph,
9+
Bet,
10+
}
11+
declare var alphabet: Alphabet;
12+
13+
// Basic: union literal key lifts to union of object types
14+
const x: { a: string } | { b: string } = { [ab]: 'hi' }
15+
16+
// Multiple unions create cross-product
17+
const y: { a: string, m: number, c: string }
18+
| { a: string, m: number, d: string }
19+
| { b: string, m: number, c: string }
20+
| { b: string, m: number, d: string } = { [ab]: 'hi', m: 1, [cd]: 'there' }
21+
22+
// Union + spread
23+
const s: { a: string, c: string } | { b: string, c: string } = { [ab]: 'hi', ...{ c: 'no' }}
24+
25+
// Number literal union
26+
const n: { "1": string } | { "2": string } = { [onetwo]: 'hi' }
27+
28+
// Enum literal union
29+
const e: { "0": string } | { "1": string } = { [alphabet]: 'hi' }
30+
31+
// Soundness check: accessing non-existent property should be error
32+
const obj = { [ab]: 1 }
33+
// obj should be { a: number } | { b: number }, not { a: number; b: number }
34+
// So accessing both .a and .b should require a type guard
35+
36+
// Methods and getters alongside union computed property
37+
const m: { a: string, m(): number, p: number } | { b: string, m(): number, p: number } =
38+
{ [ab]: 'hi', m() { return 1 }, get p() { return 2 } }
39+
40+
41+
//// [computedPropertyUnionLiftsToUnionType.js]
42+
"use strict";
43+
var Alphabet;
44+
(function (Alphabet) {
45+
Alphabet[Alphabet["Aleph"] = 0] = "Aleph";
46+
Alphabet[Alphabet["Bet"] = 1] = "Bet";
47+
})(Alphabet || (Alphabet = {}));
48+
// Basic: union literal key lifts to union of object types
49+
const x = { [ab]: 'hi' };
50+
// Multiple unions create cross-product
51+
const y = { [ab]: 'hi', m: 1, [cd]: 'there' };
52+
// Union + spread
53+
const s = Object.assign({ [ab]: 'hi' }, { c: 'no' });
54+
// Number literal union
55+
const n = { [onetwo]: 'hi' };
56+
// Enum literal union
57+
const e = { [alphabet]: 'hi' };
58+
// Soundness check: accessing non-existent property should be error
59+
const obj = { [ab]: 1 };
60+
// obj should be { a: number } | { b: number }, not { a: number; b: number }
61+
// So accessing both .a and .b should require a type guard
62+
// Methods and getters alongside union computed property
63+
const m = { [ab]: 'hi', m() { return 1; }, get p() { return 2; } };
64+
65+
66+
//// [computedPropertyUnionLiftsToUnionType.d.ts]
67+
declare var ab: 'a' | 'b';
68+
declare var cd: 'c' | 'd';
69+
declare var onetwo: 1 | 2;
70+
declare enum Alphabet {
71+
Aleph = 0,
72+
Bet = 1
73+
}
74+
declare var alphabet: Alphabet;
75+
declare const x: {
76+
a: string;
77+
} | {
78+
b: string;
79+
};
80+
declare const y: {
81+
a: string;
82+
m: number;
83+
c: string;
84+
} | {
85+
a: string;
86+
m: number;
87+
d: string;
88+
} | {
89+
b: string;
90+
m: number;
91+
c: string;
92+
} | {
93+
b: string;
94+
m: number;
95+
d: string;
96+
};
97+
declare const s: {
98+
a: string;
99+
c: string;
100+
} | {
101+
b: string;
102+
c: string;
103+
};
104+
declare const n: {
105+
"1": string;
106+
} | {
107+
"2": string;
108+
};
109+
declare const e: {
110+
"0": string;
111+
} | {
112+
"1": string;
113+
};
114+
declare const obj: {
115+
a: number;
116+
} | {
117+
b: number;
118+
};
119+
declare const m: {
120+
a: string;
121+
m(): number;
122+
p: number;
123+
} | {
124+
b: string;
125+
m(): number;
126+
p: number;
127+
};
Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,112 @@
1+
//// [tests/cases/conformance/es6/computedProperties/computedPropertyUnionLiftsToUnionType.ts] ////
2+
3+
=== computedPropertyUnionLiftsToUnionType.ts ===
4+
declare var ab: 'a' | 'b';
5+
>ab : Symbol(ab, Decl(computedPropertyUnionLiftsToUnionType.ts, 0, 11))
6+
7+
declare var cd: 'c' | 'd';
8+
>cd : Symbol(cd, Decl(computedPropertyUnionLiftsToUnionType.ts, 1, 11))
9+
10+
declare var onetwo: 1 | 2;
11+
>onetwo : Symbol(onetwo, Decl(computedPropertyUnionLiftsToUnionType.ts, 2, 11))
12+
13+
enum Alphabet {
14+
>Alphabet : Symbol(Alphabet, Decl(computedPropertyUnionLiftsToUnionType.ts, 2, 26))
15+
16+
Aleph,
17+
>Aleph : Symbol(Alphabet.Aleph, Decl(computedPropertyUnionLiftsToUnionType.ts, 3, 15))
18+
19+
Bet,
20+
>Bet : Symbol(Alphabet.Bet, Decl(computedPropertyUnionLiftsToUnionType.ts, 4, 10))
21+
}
22+
declare var alphabet: Alphabet;
23+
>alphabet : Symbol(alphabet, Decl(computedPropertyUnionLiftsToUnionType.ts, 7, 11))
24+
>Alphabet : Symbol(Alphabet, Decl(computedPropertyUnionLiftsToUnionType.ts, 2, 26))
25+
26+
// Basic: union literal key lifts to union of object types
27+
const x: { a: string } | { b: string } = { [ab]: 'hi' }
28+
>x : Symbol(x, Decl(computedPropertyUnionLiftsToUnionType.ts, 10, 5))
29+
>a : Symbol(a, Decl(computedPropertyUnionLiftsToUnionType.ts, 10, 10))
30+
>b : Symbol(b, Decl(computedPropertyUnionLiftsToUnionType.ts, 10, 26))
31+
>[ab] : Symbol([ab], Decl(computedPropertyUnionLiftsToUnionType.ts, 10, 42))
32+
>ab : Symbol(ab, Decl(computedPropertyUnionLiftsToUnionType.ts, 0, 11))
33+
34+
// Multiple unions create cross-product
35+
const y: { a: string, m: number, c: string }
36+
>y : Symbol(y, Decl(computedPropertyUnionLiftsToUnionType.ts, 13, 5))
37+
>a : Symbol(a, Decl(computedPropertyUnionLiftsToUnionType.ts, 13, 10))
38+
>m : Symbol(m, Decl(computedPropertyUnionLiftsToUnionType.ts, 13, 21))
39+
>c : Symbol(c, Decl(computedPropertyUnionLiftsToUnionType.ts, 13, 32))
40+
41+
| { a: string, m: number, d: string }
42+
>a : Symbol(a, Decl(computedPropertyUnionLiftsToUnionType.ts, 14, 7))
43+
>m : Symbol(m, Decl(computedPropertyUnionLiftsToUnionType.ts, 14, 18))
44+
>d : Symbol(d, Decl(computedPropertyUnionLiftsToUnionType.ts, 14, 29))
45+
46+
| { b: string, m: number, c: string }
47+
>b : Symbol(b, Decl(computedPropertyUnionLiftsToUnionType.ts, 15, 7))
48+
>m : Symbol(m, Decl(computedPropertyUnionLiftsToUnionType.ts, 15, 18))
49+
>c : Symbol(c, Decl(computedPropertyUnionLiftsToUnionType.ts, 15, 29))
50+
51+
| { b: string, m: number, d: string } = { [ab]: 'hi', m: 1, [cd]: 'there' }
52+
>b : Symbol(b, Decl(computedPropertyUnionLiftsToUnionType.ts, 16, 7))
53+
>m : Symbol(m, Decl(computedPropertyUnionLiftsToUnionType.ts, 16, 18))
54+
>d : Symbol(d, Decl(computedPropertyUnionLiftsToUnionType.ts, 16, 29))
55+
>[ab] : Symbol([ab], Decl(computedPropertyUnionLiftsToUnionType.ts, 16, 45))
56+
>ab : Symbol(ab, Decl(computedPropertyUnionLiftsToUnionType.ts, 0, 11))
57+
>m : Symbol(m, Decl(computedPropertyUnionLiftsToUnionType.ts, 16, 57))
58+
>[cd] : Symbol([cd], Decl(computedPropertyUnionLiftsToUnionType.ts, 16, 63))
59+
>cd : Symbol(cd, Decl(computedPropertyUnionLiftsToUnionType.ts, 1, 11))
60+
61+
// Union + spread
62+
const s: { a: string, c: string } | { b: string, c: string } = { [ab]: 'hi', ...{ c: 'no' }}
63+
>s : Symbol(s, Decl(computedPropertyUnionLiftsToUnionType.ts, 19, 5))
64+
>a : Symbol(a, Decl(computedPropertyUnionLiftsToUnionType.ts, 19, 10))
65+
>c : Symbol(c, Decl(computedPropertyUnionLiftsToUnionType.ts, 19, 21))
66+
>b : Symbol(b, Decl(computedPropertyUnionLiftsToUnionType.ts, 19, 37))
67+
>c : Symbol(c, Decl(computedPropertyUnionLiftsToUnionType.ts, 19, 48))
68+
>[ab] : Symbol([ab], Decl(computedPropertyUnionLiftsToUnionType.ts, 19, 64))
69+
>ab : Symbol(ab, Decl(computedPropertyUnionLiftsToUnionType.ts, 0, 11))
70+
>c : Symbol(c, Decl(computedPropertyUnionLiftsToUnionType.ts, 19, 81))
71+
72+
// Number literal union
73+
const n: { "1": string } | { "2": string } = { [onetwo]: 'hi' }
74+
>n : Symbol(n, Decl(computedPropertyUnionLiftsToUnionType.ts, 22, 5))
75+
>"1" : Symbol("1", Decl(computedPropertyUnionLiftsToUnionType.ts, 22, 10))
76+
>"2" : Symbol("2", Decl(computedPropertyUnionLiftsToUnionType.ts, 22, 28))
77+
>[onetwo] : Symbol([onetwo], Decl(computedPropertyUnionLiftsToUnionType.ts, 22, 46))
78+
>onetwo : Symbol(onetwo, Decl(computedPropertyUnionLiftsToUnionType.ts, 2, 11))
79+
80+
// Enum literal union
81+
const e: { "0": string } | { "1": string } = { [alphabet]: 'hi' }
82+
>e : Symbol(e, Decl(computedPropertyUnionLiftsToUnionType.ts, 25, 5))
83+
>"0" : Symbol("0", Decl(computedPropertyUnionLiftsToUnionType.ts, 25, 10))
84+
>"1" : Symbol("1", Decl(computedPropertyUnionLiftsToUnionType.ts, 25, 28))
85+
>[alphabet] : Symbol([alphabet], Decl(computedPropertyUnionLiftsToUnionType.ts, 25, 46))
86+
>alphabet : Symbol(alphabet, Decl(computedPropertyUnionLiftsToUnionType.ts, 7, 11))
87+
88+
// Soundness check: accessing non-existent property should be error
89+
const obj = { [ab]: 1 }
90+
>obj : Symbol(obj, Decl(computedPropertyUnionLiftsToUnionType.ts, 28, 5))
91+
>[ab] : Symbol([ab], Decl(computedPropertyUnionLiftsToUnionType.ts, 28, 13))
92+
>ab : Symbol(ab, Decl(computedPropertyUnionLiftsToUnionType.ts, 0, 11))
93+
94+
// obj should be { a: number } | { b: number }, not { a: number; b: number }
95+
// So accessing both .a and .b should require a type guard
96+
97+
// Methods and getters alongside union computed property
98+
const m: { a: string, m(): number, p: number } | { b: string, m(): number, p: number } =
99+
>m : Symbol(m, Decl(computedPropertyUnionLiftsToUnionType.ts, 33, 5))
100+
>a : Symbol(a, Decl(computedPropertyUnionLiftsToUnionType.ts, 33, 10))
101+
>m : Symbol(m, Decl(computedPropertyUnionLiftsToUnionType.ts, 33, 21))
102+
>p : Symbol(p, Decl(computedPropertyUnionLiftsToUnionType.ts, 33, 34))
103+
>b : Symbol(b, Decl(computedPropertyUnionLiftsToUnionType.ts, 33, 50))
104+
>m : Symbol(m, Decl(computedPropertyUnionLiftsToUnionType.ts, 33, 61))
105+
>p : Symbol(p, Decl(computedPropertyUnionLiftsToUnionType.ts, 33, 74))
106+
107+
{ [ab]: 'hi', m() { return 1 }, get p() { return 2 } }
108+
>[ab] : Symbol([ab], Decl(computedPropertyUnionLiftsToUnionType.ts, 34, 5))
109+
>ab : Symbol(ab, Decl(computedPropertyUnionLiftsToUnionType.ts, 0, 11))
110+
>m : Symbol(m, Decl(computedPropertyUnionLiftsToUnionType.ts, 34, 17))
111+
>p : Symbol(p, Decl(computedPropertyUnionLiftsToUnionType.ts, 34, 35))
112+

0 commit comments

Comments
 (0)