Skip to content

Allow distinct string enum members with identical property names to form unions in mapped types #39101

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 1 commit into from
Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 26 additions & 16 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 = <MappedSymbol>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 = <MappedSymbol>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);
Expand Down
59 changes: 59 additions & 0 deletions tests/baselines/reference/mappedTypeOverlappingStringEnumKeys.js
Original file line number Diff line number Diff line change
@@ -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<Cats, { type: V }>[]
};

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: []
};
Original file line number Diff line number Diff line change
@@ -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<Cats, { type: V }>[]
>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))

};

Original file line number Diff line number Diff line change
@@ -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<Cats, { type: V }>[]
>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[]

};

Original file line number Diff line number Diff line change
@@ -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<Cats, { type: V }>[]
};

const catMap: CatMap = {
cat: [
{ type: TerrestrialAnimalTypes.CAT, address: "" },
{ type: AlienAnimalTypes.CAT, planet: "" }
],
dog: [] as never[]
};