From 8a26d21f090cd88a8627d4e12bf0a10b58fdf04c Mon Sep 17 00:00:00 2001 From: Andrew Branch Date: Tue, 16 Jun 2020 16:46:33 -0700 Subject: [PATCH] Allow distinct string enum members with identical property names to form unions in mapped types --- src/compiler/checker.ts | 42 ++++---- .../mappedTypeOverlappingStringEnumKeys.js | 59 ++++++++++++ ...appedTypeOverlappingStringEnumKeys.symbols | 96 +++++++++++++++++++ .../mappedTypeOverlappingStringEnumKeys.types | 91 ++++++++++++++++++ .../mappedTypeOverlappingStringEnumKeys.ts | 36 +++++++ 5 files changed, 308 insertions(+), 16 deletions(-) create mode 100644 tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.js create mode 100644 tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.symbols create mode 100644 tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.types create mode 100644 tests/cases/conformance/types/mapped/mappedTypeOverlappingStringEnumKeys.ts diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index fe4b3433a9909..a104c71cfabc6 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -10131,22 +10131,32 @@ namespace ts { // Otherwise, for type string create a string index signature. if (isTypeUsableAsPropertyName(t)) { const propName = getPropertyNameFromType(t); - const modifiersProp = getPropertyOfType(modifiersType, propName); - const isOptional = !!(templateModifiers & MappedTypeModifiers.IncludeOptional || - !(templateModifiers & MappedTypeModifiers.ExcludeOptional) && modifiersProp && modifiersProp.flags & SymbolFlags.Optional); - const isReadonly = !!(templateModifiers & MappedTypeModifiers.IncludeReadonly || - !(templateModifiers & MappedTypeModifiers.ExcludeReadonly) && modifiersProp && isReadonlySymbol(modifiersProp)); - const stripOptional = strictNullChecks && !isOptional && modifiersProp && modifiersProp.flags & SymbolFlags.Optional; - const prop = createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName, - CheckFlags.Mapped | (isReadonly ? CheckFlags.Readonly : 0) | (stripOptional ? CheckFlags.StripOptional : 0)); - prop.mappedType = type; - prop.mapper = templateMapper; - if (modifiersProp) { - prop.syntheticOrigin = modifiersProp; - prop.declarations = modifiersProp.declarations; - } - prop.nameType = t; - members.set(propName, prop); + // String enum members from separate enums with identical values + // are distinct types with the same property name. Make the resulting + // property symbol's name type be the union of those enum member types. + const existingProp = members.get(propName) as MappedSymbol | undefined; + if (existingProp) { + existingProp.nameType = getUnionType([existingProp.nameType!, t]); + existingProp.mapper = appendTypeMapping(type.mapper, typeParameter, existingProp.nameType); + } + else { + const modifiersProp = getPropertyOfType(modifiersType, propName); + const isOptional = !!(templateModifiers & MappedTypeModifiers.IncludeOptional || + !(templateModifiers & MappedTypeModifiers.ExcludeOptional) && modifiersProp && modifiersProp.flags & SymbolFlags.Optional); + const isReadonly = !!(templateModifiers & MappedTypeModifiers.IncludeReadonly || + !(templateModifiers & MappedTypeModifiers.ExcludeReadonly) && modifiersProp && isReadonlySymbol(modifiersProp)); + const stripOptional = strictNullChecks && !isOptional && modifiersProp && modifiersProp.flags & SymbolFlags.Optional; + const prop = createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName, + CheckFlags.Mapped | (isReadonly ? CheckFlags.Readonly : 0) | (stripOptional ? CheckFlags.StripOptional : 0)); + prop.mappedType = type; + if (modifiersProp) { + prop.syntheticOrigin = modifiersProp; + prop.declarations = modifiersProp.declarations; + } + prop.nameType = t; + prop.mapper = templateMapper; + members.set(propName, prop); + } } else if (t.flags & (TypeFlags.Any | TypeFlags.String | TypeFlags.Number | TypeFlags.Enum)) { const propType = instantiateType(templateType, templateMapper); diff --git a/tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.js b/tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.js new file mode 100644 index 0000000000000..d25fde357ad66 --- /dev/null +++ b/tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.js @@ -0,0 +1,59 @@ +//// [mappedTypeOverlappingStringEnumKeys.ts] +// #37859 + +enum TerrestrialAnimalTypes { + CAT = "cat", + DOG = "dog" +}; + +enum AlienAnimalTypes { + CAT = "cat", +}; + +type AnimalTypes = TerrestrialAnimalTypes | AlienAnimalTypes; + +interface TerrestrialCat { + type: TerrestrialAnimalTypes.CAT; + address: string; +} + +interface AlienCat { + type: AlienAnimalTypes.CAT + planet: string; +} + +type Cats = TerrestrialCat | AlienCat; + +type CatMap = { + [V in AnimalTypes]: Extract[] +}; + +const catMap: CatMap = { + cat: [ + { type: TerrestrialAnimalTypes.CAT, address: "" }, + { type: AlienAnimalTypes.CAT, planet: "" } + ], + dog: [] as never[] +}; + + +//// [mappedTypeOverlappingStringEnumKeys.js] +// #37859 +var TerrestrialAnimalTypes; +(function (TerrestrialAnimalTypes) { + TerrestrialAnimalTypes["CAT"] = "cat"; + TerrestrialAnimalTypes["DOG"] = "dog"; +})(TerrestrialAnimalTypes || (TerrestrialAnimalTypes = {})); +; +var AlienAnimalTypes; +(function (AlienAnimalTypes) { + AlienAnimalTypes["CAT"] = "cat"; +})(AlienAnimalTypes || (AlienAnimalTypes = {})); +; +var catMap = { + cat: [ + { type: TerrestrialAnimalTypes.CAT, address: "" }, + { type: AlienAnimalTypes.CAT, planet: "" } + ], + dog: [] +}; diff --git a/tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.symbols b/tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.symbols new file mode 100644 index 0000000000000..740cae0d7b953 --- /dev/null +++ b/tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.symbols @@ -0,0 +1,96 @@ +=== tests/cases/conformance/types/mapped/mappedTypeOverlappingStringEnumKeys.ts === +// #37859 + +enum TerrestrialAnimalTypes { +>TerrestrialAnimalTypes : Symbol(TerrestrialAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 0, 0)) + + CAT = "cat", +>CAT : Symbol(TerrestrialAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 2, 29)) + + DOG = "dog" +>DOG : Symbol(TerrestrialAnimalTypes.DOG, Decl(mappedTypeOverlappingStringEnumKeys.ts, 3, 14)) + +}; + +enum AlienAnimalTypes { +>AlienAnimalTypes : Symbol(AlienAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 5, 2)) + + CAT = "cat", +>CAT : Symbol(AlienAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 7, 23)) + +}; + +type AnimalTypes = TerrestrialAnimalTypes | AlienAnimalTypes; +>AnimalTypes : Symbol(AnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 9, 2)) +>TerrestrialAnimalTypes : Symbol(TerrestrialAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 0, 0)) +>AlienAnimalTypes : Symbol(AlienAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 5, 2)) + +interface TerrestrialCat { +>TerrestrialCat : Symbol(TerrestrialCat, Decl(mappedTypeOverlappingStringEnumKeys.ts, 11, 61)) + + type: TerrestrialAnimalTypes.CAT; +>type : Symbol(TerrestrialCat.type, Decl(mappedTypeOverlappingStringEnumKeys.ts, 13, 26)) +>TerrestrialAnimalTypes : Symbol(TerrestrialAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 0, 0)) +>CAT : Symbol(TerrestrialAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 2, 29)) + + address: string; +>address : Symbol(TerrestrialCat.address, Decl(mappedTypeOverlappingStringEnumKeys.ts, 14, 35)) +} + +interface AlienCat { +>AlienCat : Symbol(AlienCat, Decl(mappedTypeOverlappingStringEnumKeys.ts, 16, 1)) + + type: AlienAnimalTypes.CAT +>type : Symbol(AlienCat.type, Decl(mappedTypeOverlappingStringEnumKeys.ts, 18, 20)) +>AlienAnimalTypes : Symbol(AlienAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 5, 2)) +>CAT : Symbol(AlienAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 7, 23)) + + planet: string; +>planet : Symbol(AlienCat.planet, Decl(mappedTypeOverlappingStringEnumKeys.ts, 19, 28)) +} + +type Cats = TerrestrialCat | AlienCat; +>Cats : Symbol(Cats, Decl(mappedTypeOverlappingStringEnumKeys.ts, 21, 1)) +>TerrestrialCat : Symbol(TerrestrialCat, Decl(mappedTypeOverlappingStringEnumKeys.ts, 11, 61)) +>AlienCat : Symbol(AlienCat, Decl(mappedTypeOverlappingStringEnumKeys.ts, 16, 1)) + +type CatMap = { +>CatMap : Symbol(CatMap, Decl(mappedTypeOverlappingStringEnumKeys.ts, 23, 38)) + + [V in AnimalTypes]: Extract[] +>V : Symbol(V, Decl(mappedTypeOverlappingStringEnumKeys.ts, 26, 3)) +>AnimalTypes : Symbol(AnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 9, 2)) +>Extract : Symbol(Extract, Decl(lib.es5.d.ts, --, --)) +>Cats : Symbol(Cats, Decl(mappedTypeOverlappingStringEnumKeys.ts, 21, 1)) +>type : Symbol(type, Decl(mappedTypeOverlappingStringEnumKeys.ts, 26, 37)) +>V : Symbol(V, Decl(mappedTypeOverlappingStringEnumKeys.ts, 26, 3)) + +}; + +const catMap: CatMap = { +>catMap : Symbol(catMap, Decl(mappedTypeOverlappingStringEnumKeys.ts, 29, 5)) +>CatMap : Symbol(CatMap, Decl(mappedTypeOverlappingStringEnumKeys.ts, 23, 38)) + + cat: [ +>cat : Symbol(cat, Decl(mappedTypeOverlappingStringEnumKeys.ts, 29, 24)) + + { type: TerrestrialAnimalTypes.CAT, address: "" }, +>type : Symbol(type, Decl(mappedTypeOverlappingStringEnumKeys.ts, 31, 5)) +>TerrestrialAnimalTypes.CAT : Symbol(TerrestrialAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 2, 29)) +>TerrestrialAnimalTypes : Symbol(TerrestrialAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 0, 0)) +>CAT : Symbol(TerrestrialAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 2, 29)) +>address : Symbol(address, Decl(mappedTypeOverlappingStringEnumKeys.ts, 31, 39)) + + { type: AlienAnimalTypes.CAT, planet: "" } +>type : Symbol(type, Decl(mappedTypeOverlappingStringEnumKeys.ts, 32, 5)) +>AlienAnimalTypes.CAT : Symbol(AlienAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 7, 23)) +>AlienAnimalTypes : Symbol(AlienAnimalTypes, Decl(mappedTypeOverlappingStringEnumKeys.ts, 5, 2)) +>CAT : Symbol(AlienAnimalTypes.CAT, Decl(mappedTypeOverlappingStringEnumKeys.ts, 7, 23)) +>planet : Symbol(planet, Decl(mappedTypeOverlappingStringEnumKeys.ts, 32, 33)) + + ], + dog: [] as never[] +>dog : Symbol(dog, Decl(mappedTypeOverlappingStringEnumKeys.ts, 33, 4)) + +}; + diff --git a/tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.types b/tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.types new file mode 100644 index 0000000000000..ae58d3f12ddc3 --- /dev/null +++ b/tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.types @@ -0,0 +1,91 @@ +=== tests/cases/conformance/types/mapped/mappedTypeOverlappingStringEnumKeys.ts === +// #37859 + +enum TerrestrialAnimalTypes { +>TerrestrialAnimalTypes : TerrestrialAnimalTypes + + CAT = "cat", +>CAT : TerrestrialAnimalTypes.CAT +>"cat" : "cat" + + DOG = "dog" +>DOG : TerrestrialAnimalTypes.DOG +>"dog" : "dog" + +}; + +enum AlienAnimalTypes { +>AlienAnimalTypes : AlienAnimalTypes + + CAT = "cat", +>CAT : AlienAnimalTypes.CAT +>"cat" : "cat" + +}; + +type AnimalTypes = TerrestrialAnimalTypes | AlienAnimalTypes; +>AnimalTypes : AnimalTypes + +interface TerrestrialCat { + type: TerrestrialAnimalTypes.CAT; +>type : TerrestrialAnimalTypes.CAT +>TerrestrialAnimalTypes : any + + address: string; +>address : string +} + +interface AlienCat { + type: AlienAnimalTypes.CAT +>type : AlienAnimalTypes +>AlienAnimalTypes : any + + planet: string; +>planet : string +} + +type Cats = TerrestrialCat | AlienCat; +>Cats : Cats + +type CatMap = { +>CatMap : CatMap + + [V in AnimalTypes]: Extract[] +>type : V + +}; + +const catMap: CatMap = { +>catMap : CatMap +>{ 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[]; } + + cat: [ +>cat : ({ type: TerrestrialAnimalTypes.CAT; address: string; } | { type: AlienAnimalTypes.CAT; planet: string; })[] +>[ { type: TerrestrialAnimalTypes.CAT, address: "" }, { type: AlienAnimalTypes.CAT, planet: "" } ] : ({ type: TerrestrialAnimalTypes.CAT; address: string; } | { type: AlienAnimalTypes.CAT; planet: string; })[] + + { type: TerrestrialAnimalTypes.CAT, address: "" }, +>{ type: TerrestrialAnimalTypes.CAT, address: "" } : { type: TerrestrialAnimalTypes.CAT; address: string; } +>type : TerrestrialAnimalTypes.CAT +>TerrestrialAnimalTypes.CAT : TerrestrialAnimalTypes.CAT +>TerrestrialAnimalTypes : typeof TerrestrialAnimalTypes +>CAT : TerrestrialAnimalTypes.CAT +>address : string +>"" : "" + + { type: AlienAnimalTypes.CAT, planet: "" } +>{ type: AlienAnimalTypes.CAT, planet: "" } : { type: AlienAnimalTypes.CAT; planet: string; } +>type : AlienAnimalTypes.CAT +>AlienAnimalTypes.CAT : AlienAnimalTypes +>AlienAnimalTypes : typeof AlienAnimalTypes +>CAT : AlienAnimalTypes +>planet : string +>"" : "" + + ], + dog: [] as never[] +>dog : never[] +>[] as never[] : never[] +>[] : undefined[] + +}; + diff --git a/tests/cases/conformance/types/mapped/mappedTypeOverlappingStringEnumKeys.ts b/tests/cases/conformance/types/mapped/mappedTypeOverlappingStringEnumKeys.ts new file mode 100644 index 0000000000000..c1ebd4b382c3c --- /dev/null +++ b/tests/cases/conformance/types/mapped/mappedTypeOverlappingStringEnumKeys.ts @@ -0,0 +1,36 @@ +// #37859 + +enum TerrestrialAnimalTypes { + CAT = "cat", + DOG = "dog" +}; + +enum AlienAnimalTypes { + CAT = "cat", +}; + +type AnimalTypes = TerrestrialAnimalTypes | AlienAnimalTypes; + +interface TerrestrialCat { + type: TerrestrialAnimalTypes.CAT; + address: string; +} + +interface AlienCat { + type: AlienAnimalTypes.CAT + planet: string; +} + +type Cats = TerrestrialCat | AlienCat; + +type CatMap = { + [V in AnimalTypes]: Extract[] +}; + +const catMap: CatMap = { + cat: [ + { type: TerrestrialAnimalTypes.CAT, address: "" }, + { type: AlienAnimalTypes.CAT, planet: "" } + ], + dog: [] as never[] +};