How to merge two enums in TypeScript
Suppose I have two enums as described below in Typescript, then How do I merge them
enum Mammals {
Humans,
Bats,
Dolphins
}
enum Reptiles {
Snakes,
Alligators,
Lizards
}
export default Mammals & Reptiles // For Illustration purpose, Consider both the Enums have been merged.
Now, when I import
the exported value
in another file, I should be able to access values from both the enums.
import animalTypes from "./animalTypes"
animalTypes.Humans //valid
animalTypes.Snakes // valid
How can I achieve such functionality in TypeScript?
Solution 1:
Problems with the merge:
- same values => values are overwritten
same keys => keys are overwritten
❌ Enums with same values (=> values are overwritten)
enum AA1 {
aKey, // = 0
bKey // = 1
}
enum BB1 {
cKey, // = 0
dKey // = 1
}
- ❌ Enums with the same keys (=> keys are overwritten)
enum AA2 {
aKey = 1
}
enum BB2 {
aKey = 2
}
- ✅ Good
enum AA3 {
aKey, // = 0
bKey // = 1
}
enum BB3 {
cKey = 2,
dKey // = 3
}
- ✅ Also Good
enum AA4 {
aKey = 'Hello',
bKey = 0,
cKey // = 1
}
enum BB4 {
dKey = 2,
eKey = 'Hello',
fKey = 'World'
}
Note:
aKey = 'Hello'
and eKey = 'Hello'
work because the enum with a string value doesn't has this value as key
// For aKey = 'Hello', key is working
type aa4aKey = AA4.aKey; // = AA4.aKey
// value is not.
type aa4aValue = AA4.Hello; // ❌ Namespace 'AA4' has no exported member 'Hello'
type aa4aValue2 = AA4['Hello']; // ❌ Property 'Hello' does not exist on type 'AA4'
console.log(AA4); // { 0: 'bKey', 1: 'cKey', aKey: 'Hello', bKey: 0, cKey: 1 }
console.log(BB4); // { 2: 'dKey', dKey: 2, eKey: 'Hello', fKey: 'World' }
The merge
- ❌ using union types
type AABB1 = AA4 | BB4; // = AA4 | BB4
type AABB1key = AABB1['aKey']; // = never
type AABB1key2 = AABB1.aKey; // ❌ 'AABB1' only refers to a type, but is being used as a namespace here. ts(2702)
- ❌ using intersection types
type AABB1 = AA4 & BB4; // = never
type AABB1key = AABB1['aKey']; // = never
- ✅ using intersection types with typeof
type AABB2 = (typeof AA4) & (typeof BB4); // = typeof AA4 & typeof BB4
type AABB2key = AABB2['aKey']; // = AA4.aKey
- ✅ using js copy
const aabb1 = { ...AA4, ...BB4 };
const aabb2 = Object.assign({}, AA4, BB4); // also work
// aabb1 = {
// 0: 'bKey',
// 1: 'cKey',
// 2: 'dKey',
// aKey: 'Hello',
// bKey: 0,
// cKey: 1,
// dKey: 2,
// eKey: 'Hello',
// fKey: 'World' }
- ✅ using typeof with a js copy
const aabb = { ...AA4, ...BB4 };
type TypeofAABB = typeof aabb;
// type TypeofAABB = {
// [x: number]: string;
// dKey: BB4.dKey;
// eKey: BB4.eKey;
// fKey: BB4.fKey;
// aKey: AA4.aKey;
// bKey: AA4.bKey;
// cKey: AA4.cKey;
// };
Tip: you can use the same name for a type and a value
const merged = { ...AA4, ...BB4 };
type merged = typeof merged;
const aValue = merged.aKey;
type aType = merged['aKey'];
Your case
If you want to merge your 2 enums you have ~3 choices:
1. Using string enums
enum Mammals {
Humans = 'Humans',
Bats = 'Bats',
Dolphins = 'Dolphins'
}
enum Reptiles {
Snakes = 'Snakes',
Alligators = 'Alligators',
Lizards = 'Lizards'
}
export const Animals = { ...Mammals, ...Reptiles };
export type Animals = typeof Animals;
2. Using unique numbers
enum Mammals {
Humans = 0,
Bats,
Dolphins
}
enum Reptiles {
Snakes = 2,
Alligators,
Lizards
}
export const Animals = { ...Mammals, ...Reptiles };
export type Animals = typeof Animals;
3. Using nested enums
enum Mammals {
Humans,
Bats,
Dolphins
}
enum Reptiles {
Snakes,
Alligators,
Lizards
}
export const Animals = { Mammals, Reptiles };
export type Animals = typeof Animals;
const bats = Animals.Mammals.Bats; // = 1
const alligators = Animals.Reptiles.Alligators; // = 1
Note: you can also merge the nested enums with the following code. Take care to NOT have duplicated values if you do that!
type Animal = {
[K in keyof Animals]: {
[K2 in keyof Animals[K]]: Animals[K][K2]
}[keyof Animals[K]]
}[keyof Animals];
const animal: Animal = 0 as any;
switch (animal) {
case Animals.Mammals.Bats:
case Animals.Mammals.Dolphins:
case Animals.Mammals.Humans:
case Animals.Reptiles.Alligators:
case Animals.Reptiles.Lizards:
case Animals.Reptiles.Snakes:
break;
default: {
const invalid: never = animal; // no error
}
}
Solution 2:
If you want something behaving like an enum from the way you consume it, you could still use merged object in javascript.
enum Mammals {
Humans = 'Humans',
Bats = 'Bats',
Dolphins = 'Dolphins',
}
enum Reptiles {
Snakes = 'Snakes',
Alligators = 'Alligators',
Lizards = 'Lizards',
}
const Animals = {
...Mammals,
...Reptiles,
}
type Animals = Mammals | Reptiles
Then you could use Animals.Snakes or Animals.Dolphins and both should be properly typed and work as an enum
Solution 3:
Enums, interfaces and types - a working solution for merging enums
What's confusing here is types vs. values.
- If you define a value (
let
,const
etc.) it will have a value plus some computed but not separately named type. - If you define a
type
orinterface
, it will create a named type but that will not be outputted or considered in the final JS in any way. It only helps when writing your app. - If you create an
enum
in Typescript, it creates a static type name that you can use plus a real object outputted to JS that you can use.
From the TS handbook:
Using an enum is simple: just access any member as a property off of the enum itself, and declare types using the name of the enum.
So, if you Object.assign()
two enums, it will create a new, merged value (object), but not a new named type.
Since it's not an enum
anymore, you lose the advantage of having a value and a named type, but you can still create a separate type name as a workaround.
Fortunately, you can have the same name for the value and the type, and TS will import both if you export them.
// This creates a merged enum, but not a type
const Animals = Object.assign({}, Mammals, Reptiles);
// Workaround: create a named type (typeof Animals won't work here!)
type Animals = Mammals | Reptiles;
TS playground link