Skip to content

Commit 58ed610

Browse files
authored
Allow distinct string enum members with identical property names to form unions in mapped types (microsoft#39101)
1 parent 3853bf5 commit 58ed610

File tree

5 files changed

+308
-16
lines changed

5 files changed

+308
-16
lines changed

src/compiler/checker.ts

+26-16
Original file line numberDiff line numberDiff line change
@@ -10253,22 +10253,32 @@ namespace ts {
1025310253
// Otherwise, for type string create a string index signature.
1025410254
if (isTypeUsableAsPropertyName(t)) {
1025510255
const propName = getPropertyNameFromType(t);
10256-
const modifiersProp = getPropertyOfType(modifiersType, propName);
10257-
const isOptional = !!(templateModifiers & MappedTypeModifiers.IncludeOptional ||
10258-
!(templateModifiers & MappedTypeModifiers.ExcludeOptional) && modifiersProp && modifiersProp.flags & SymbolFlags.Optional);
10259-
const isReadonly = !!(templateModifiers & MappedTypeModifiers.IncludeReadonly ||
10260-
!(templateModifiers & MappedTypeModifiers.ExcludeReadonly) && modifiersProp && isReadonlySymbol(modifiersProp));
10261-
const stripOptional = strictNullChecks && !isOptional && modifiersProp && modifiersProp.flags & SymbolFlags.Optional;
10262-
const prop = <MappedSymbol>createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName,
10263-
CheckFlags.Mapped | (isReadonly ? CheckFlags.Readonly : 0) | (stripOptional ? CheckFlags.StripOptional : 0));
10264-
prop.mappedType = type;
10265-
prop.mapper = templateMapper;
10266-
if (modifiersProp) {
10267-
prop.syntheticOrigin = modifiersProp;
10268-
prop.declarations = modifiersProp.declarations;
10269-
}
10270-
prop.nameType = t;
10271-
members.set(propName, prop);
10256+
// String enum members from separate enums with identical values
10257+
// are distinct types with the same property name. Make the resulting
10258+
// property symbol's name type be the union of those enum member types.
10259+
const existingProp = members.get(propName) as MappedSymbol | undefined;
10260+
if (existingProp) {
10261+
existingProp.nameType = getUnionType([existingProp.nameType!, t]);
10262+
existingProp.mapper = appendTypeMapping(type.mapper, typeParameter, existingProp.nameType);
10263+
}
10264+
else {
10265+
const modifiersProp = getPropertyOfType(modifiersType, propName);
10266+
const isOptional = !!(templateModifiers & MappedTypeModifiers.IncludeOptional ||
10267+
!(templateModifiers & MappedTypeModifiers.ExcludeOptional) && modifiersProp && modifiersProp.flags & SymbolFlags.Optional);
10268+
const isReadonly = !!(templateModifiers & MappedTypeModifiers.IncludeReadonly ||
10269+
!(templateModifiers & MappedTypeModifiers.ExcludeReadonly) && modifiersProp && isReadonlySymbol(modifiersProp));
10270+
const stripOptional = strictNullChecks && !isOptional && modifiersProp && modifiersProp.flags & SymbolFlags.Optional;
10271+
const prop = <MappedSymbol>createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName,
10272+
CheckFlags.Mapped | (isReadonly ? CheckFlags.Readonly : 0) | (stripOptional ? CheckFlags.StripOptional : 0));
10273+
prop.mappedType = type;
10274+
if (modifiersProp) {
10275+
prop.syntheticOrigin = modifiersProp;
10276+
prop.declarations = modifiersProp.declarations;
10277+
}
10278+
prop.nameType = t;
10279+
prop.mapper = templateMapper;
10280+
members.set(propName, prop);
10281+
}
1027210282
}
1027310283
else if (t.flags & (TypeFlags.Any | TypeFlags.String | TypeFlags.Number | TypeFlags.Enum)) {
1027410284
const propType = instantiateType(templateType, templateMapper);
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
//// [mappedTypeOverlappingStringEnumKeys.ts]
2+
// #37859
3+
4+
enum TerrestrialAnimalTypes {
5+
CAT = "cat",
6+
DOG = "dog"
7+
};
8+
9+
enum AlienAnimalTypes {
10+
CAT = "cat",
11+
};
12+
13+
type AnimalTypes = TerrestrialAnimalTypes | AlienAnimalTypes;
14+
15+
interface TerrestrialCat {
16+
type: TerrestrialAnimalTypes.CAT;
17+
address: string;
18+
}
19+
20+
interface AlienCat {
21+
type: AlienAnimalTypes.CAT
22+
planet: string;
23+
}
24+
25+
type Cats = TerrestrialCat | AlienCat;
26+
27+
type CatMap = {
28+
[V in AnimalTypes]: Extract<Cats, { type: V }>[]
29+
};
30+
31+
const catMap: CatMap = {
32+
cat: [
33+
{ type: TerrestrialAnimalTypes.CAT, address: "" },
34+
{ type: AlienAnimalTypes.CAT, planet: "" }
35+
],
36+
dog: [] as never[]
37+
};
38+
39+
40+
//// [mappedTypeOverlappingStringEnumKeys.js]
41+
// #37859
42+
var TerrestrialAnimalTypes;
43+
(function (TerrestrialAnimalTypes) {
44+
TerrestrialAnimalTypes["CAT"] = "cat";
45+
TerrestrialAnimalTypes["DOG"] = "dog";
46+
})(TerrestrialAnimalTypes || (TerrestrialAnimalTypes = {}));
47+
;
48+
var AlienAnimalTypes;
49+
(function (AlienAnimalTypes) {
50+
AlienAnimalTypes["CAT"] = "cat";
51+
})(AlienAnimalTypes || (AlienAnimalTypes = {}));
52+
;
53+
var catMap = {
54+
cat: [
55+
{ type: TerrestrialAnimalTypes.CAT, address: "" },
56+
{ type: AlienAnimalTypes.CAT, planet: "" }
57+
],
58+
dog: []
59+
};
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
=== tests/cases/conformance/types/mapped/mappedTypeOverlappingStringEnumKeys.ts ===
2+
// #37859
3+
4+
enum TerrestrialAnimalTypes {
5+
>TerrestrialAnimalTypes : Symbol(TerrestrialAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 0, 0))
6+
7+
CAT = "cat",
8+
>CAT : Symbol(TerrestrialAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 2, 29))
9+
10+
DOG = "dog"
11+
>DOG : Symbol(TerrestrialAnimalTypes.DOG, Decl(mappedTypeOverlappingStringEnumKeys.ts, 3, 14))
12+
13+
};
14+
15+
enum AlienAnimalTypes {
16+
>AlienAnimalTypes : Symbol(AlienAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 5, 2))
17+
18+
CAT = "cat",
19+
>CAT : Symbol(AlienAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 7, 23))
20+
21+
};
22+
23+
type AnimalTypes = TerrestrialAnimalTypes | AlienAnimalTypes;
24+
>AnimalTypes : Symbol(AnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 9, 2))
25+
>TerrestrialAnimalTypes : Symbol(TerrestrialAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 0, 0))
26+
>AlienAnimalTypes : Symbol(AlienAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 5, 2))
27+
28+
interface TerrestrialCat {
29+
>TerrestrialCat : Symbol(TerrestrialCat, Decl(mappedTypeOverlappingStringEnumKeys.ts, 11, 61))
30+
31+
type: TerrestrialAnimalTypes.CAT;
32+
>type : Symbol(TerrestrialCat.type, Decl(mappedTypeOverlappingStringEnumKeys.ts, 13, 26))
33+
>TerrestrialAnimalTypes : Symbol(TerrestrialAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 0, 0))
34+
>CAT : Symbol(TerrestrialAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 2, 29))
35+
36+
address: string;
37+
>address : Symbol(TerrestrialCat.address, Decl(mappedTypeOverlappingStringEnumKeys.ts, 14, 35))
38+
}
39+
40+
interface AlienCat {
41+
>AlienCat : Symbol(AlienCat, Decl(mappedTypeOverlappingStringEnumKeys.ts, 16, 1))
42+
43+
type: AlienAnimalTypes.CAT
44+
>type : Symbol(AlienCat.type, Decl(mappedTypeOverlappingStringEnumKeys.ts, 18, 20))
45+
>AlienAnimalTypes : Symbol(AlienAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 5, 2))
46+
>CAT : Symbol(AlienAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 7, 23))
47+
48+
planet: string;
49+
>planet : Symbol(AlienCat.planet, Decl(mappedTypeOverlappingStringEnumKeys.ts, 19, 28))
50+
}
51+
52+
type Cats = TerrestrialCat | AlienCat;
53+
>Cats : Symbol(Cats, Decl(mappedTypeOverlappingStringEnumKeys.ts, 21, 1))
54+
>TerrestrialCat : Symbol(TerrestrialCat, Decl(mappedTypeOverlappingStringEnumKeys.ts, 11, 61))
55+
>AlienCat : Symbol(AlienCat, Decl(mappedTypeOverlappingStringEnumKeys.ts, 16, 1))
56+
57+
type CatMap = {
58+
>CatMap : Symbol(CatMap, Decl(mappedTypeOverlappingStringEnumKeys.ts, 23, 38))
59+
60+
[V in AnimalTypes]: Extract<Cats, { type: V }>[]
61+
>V : Symbol(V, Decl(mappedTypeOverlappingStringEnumKeys.ts, 26, 3))
62+
>AnimalTypes : Symbol(AnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 9, 2))
63+
>Extract : Symbol(Extract, Decl(lib.es5.d.ts, --, --))
64+
>Cats : Symbol(Cats, Decl(mappedTypeOverlappingStringEnumKeys.ts, 21, 1))
65+
>type : Symbol(type, Decl(mappedTypeOverlappingStringEnumKeys.ts, 26, 37))
66+
>V : Symbol(V, Decl(mappedTypeOverlappingStringEnumKeys.ts, 26, 3))
67+
68+
};
69+
70+
const catMap: CatMap = {
71+
>catMap : Symbol(catMap, Decl(mappedTypeOverlappingStringEnumKeys.ts, 29, 5))
72+
>CatMap : Symbol(CatMap, Decl(mappedTypeOverlappingStringEnumKeys.ts, 23, 38))
73+
74+
cat: [
75+
>cat : Symbol(cat, Decl(mappedTypeOverlappingStringEnumKeys.ts, 29, 24))
76+
77+
{ type: TerrestrialAnimalTypes.CAT, address: "" },
78+
>type : Symbol(type, Decl(mappedTypeOverlappingStringEnumKeys.ts, 31, 5))
79+
>TerrestrialAnimalTypes.CAT : Symbol(TerrestrialAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 2, 29))
80+
>TerrestrialAnimalTypes : Symbol(TerrestrialAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 0, 0))
81+
>CAT : Symbol(TerrestrialAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 2, 29))
82+
>address : Symbol(address, Decl(mappedTypeOverlappingStringEnumKeys.ts, 31, 39))
83+
84+
{ type: AlienAnimalTypes.CAT, planet: "" }
85+
>type : Symbol(type, Decl(mappedTypeOverlappingStringEnumKeys.ts, 32, 5))
86+
>AlienAnimalTypes.CAT : Symbol(AlienAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 7, 23))
87+
>AlienAnimalTypes : Symbol(AlienAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 5, 2))
88+
>CAT : Symbol(AlienAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 7, 23))
89+
>planet : Symbol(planet, Decl(mappedTypeOverlappingStringEnumKeys.ts, 32, 33))
90+
91+
],
92+
dog: [] as never[]
93+
>dog : Symbol(dog, Decl(mappedTypeOverlappingStringEnumKeys.ts, 33, 4))
94+
95+
};
96+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
=== tests/cases/conformance/types/mapped/mappedTypeOverlappingStringEnumKeys.ts ===
2+
// #37859
3+
4+
enum TerrestrialAnimalTypes {
5+
>TerrestrialAnimalTypes : TerrestrialAnimalTypes
6+
7+
CAT = "cat",
8+
>CAT : TerrestrialAnimalTypes.CAT
9+
>"cat" : "cat"
10+
11+
DOG = "dog"
12+
>DOG : TerrestrialAnimalTypes.DOG
13+
>"dog" : "dog"
14+
15+
};
16+
17+
enum AlienAnimalTypes {
18+
>AlienAnimalTypes : AlienAnimalTypes
19+
20+
CAT = "cat",
21+
>CAT : AlienAnimalTypes.CAT
22+
>"cat" : "cat"
23+
24+
};
25+
26+
type AnimalTypes = TerrestrialAnimalTypes | AlienAnimalTypes;
27+
>AnimalTypes : AnimalTypes
28+
29+
interface TerrestrialCat {
30+
type: TerrestrialAnimalTypes.CAT;
31+
>type : TerrestrialAnimalTypes.CAT
32+
>TerrestrialAnimalTypes : any
33+
34+
address: string;
35+
>address : string
36+
}
37+
38+
interface AlienCat {
39+
type: AlienAnimalTypes.CAT
40+
>type : AlienAnimalTypes
41+
>AlienAnimalTypes : any
42+
43+
planet: string;
44+
>planet : string
45+
}
46+
47+
type Cats = TerrestrialCat | AlienCat;
48+
>Cats : Cats
49+
50+
type CatMap = {
51+
>CatMap : CatMap
52+
53+
[V in AnimalTypes]: Extract<Cats, { type: V }>[]
54+
>type : V
55+
56+
};
57+
58+
const catMap: CatMap = {
59+
>catMap : CatMap
60+
>{ cat: [ { type: TerrestrialAnimalTypes.CAT, address: "" }, { type: AlienAnimalTypes.CAT, planet: "" } ], dog: [] as never[]} : { cat: ({ type: TerrestrialAnimalTypes.CAT; address: string; } | { type: AlienAnimalTypes.CAT; planet: string; })[]; dog: never[]; }
61+
62+
cat: [
63+
>cat : ({ type: TerrestrialAnimalTypes.CAT; address: string; } | { type: AlienAnimalTypes.CAT; planet: string; })[]
64+
>[ { type: TerrestrialAnimalTypes.CAT, address: "" }, { type: AlienAnimalTypes.CAT, planet: "" } ] : ({ type: TerrestrialAnimalTypes.CAT; address: string; } | { type: AlienAnimalTypes.CAT; planet: string; })[]
65+
66+
{ type: TerrestrialAnimalTypes.CAT, address: "" },
67+
>{ type: TerrestrialAnimalTypes.CAT, address: "" } : { type: TerrestrialAnimalTypes.CAT; address: string; }
68+
>type : TerrestrialAnimalTypes.CAT
69+
>TerrestrialAnimalTypes.CAT : TerrestrialAnimalTypes.CAT
70+
>TerrestrialAnimalTypes : typeof TerrestrialAnimalTypes
71+
>CAT : TerrestrialAnimalTypes.CAT
72+
>address : string
73+
>"" : ""
74+
75+
{ type: AlienAnimalTypes.CAT, planet: "" }
76+
>{ type: AlienAnimalTypes.CAT, planet: "" } : { type: AlienAnimalTypes.CAT; planet: string; }
77+
>type : AlienAnimalTypes.CAT
78+
>AlienAnimalTypes.CAT : AlienAnimalTypes
79+
>AlienAnimalTypes : typeof AlienAnimalTypes
80+
>CAT : AlienAnimalTypes
81+
>planet : string
82+
>"" : ""
83+
84+
],
85+
dog: [] as never[]
86+
>dog : never[]
87+
>[] as never[] : never[]
88+
>[] : undefined[]
89+
90+
};
91+
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
// #37859
2+
3+
enum TerrestrialAnimalTypes {
4+
CAT = "cat",
5+
DOG = "dog"
6+
};
7+
8+
enum AlienAnimalTypes {
9+
CAT = "cat",
10+
};
11+
12+
type AnimalTypes = TerrestrialAnimalTypes | AlienAnimalTypes;
13+
14+
interface TerrestrialCat {
15+
type: TerrestrialAnimalTypes.CAT;
16+
address: string;
17+
}
18+
19+
interface AlienCat {
20+
type: AlienAnimalTypes.CAT
21+
planet: string;
22+
}
23+
24+
type Cats = TerrestrialCat | AlienCat;
25+
26+
type CatMap = {
27+
[V in AnimalTypes]: Extract<Cats, { type: V }>[]
28+
};
29+
30+
const catMap: CatMap = {
31+
cat: [
32+
{ type: TerrestrialAnimalTypes.CAT, address: "" },
33+
{ type: AlienAnimalTypes.CAT, planet: "" }
34+
],
35+
dog: [] as never[]
36+
};

0 commit comments

Comments
 (0)