How to merge two enums in TypeScript

55,276

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 or interface, 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

Solution 4

A TypeScript enum not only contains the keys you define but also the numerical inverse, so for example:

Mammals.Humans === 0 && Mammals[0] === 'Humans'

Now, if you try to merge them -- for example with Object#assign -- you'd end up with two keys having the same numerical value:

const AnimalTypes = Object.assign({}, Mammals, Reptiles);
console.log(AnimalTypes.Humans === AnimalTypes.Snakes) // true

And I suppose that's not what you want.

One way to prevent this, is to manually assign the values to the enum and make sure that they are different:

enum Mammals {
    Humans = 0,
    Bats = 1,
    Dolphins = 2
}

enum Reptiles {
    Snakes = 3,
    Alligators = 4,
    Lizards = 5
}

or less explicit but otherwise equivalent:

enum Mammals {
    Humans,
    Bats,
    Dolphins
}

enum Reptiles {
    Snakes = 3,
    Alligators,
    Lizards
}

Anyway, as long as you make sure that the enums you merge have different key/value sets you can merge them with Object#assign.

Playground Demo

Solution 5

I'd say the proper way to do it would be defining a new type:

enum Mammals {
    Humans = 'Humans',
    Bats = 'Bats',
    Dolphins = 'Dolphins',
}

enum Reptiles {
  Snakes = 'Snakes',
  Alligators = 'Alligators',
  Lizards = 'Lizards',
}

type Animals = Mammals | Reptiles;
Share:
55,276
besrabasant
Author by

besrabasant

Updated on July 05, 2022

Comments

  • besrabasant
    besrabasant almost 2 years

    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?

  • besrabasant
    besrabasant over 6 years
    The merging seems to works, But I cannot use that in an interface. Playground Demo below. @Tao
  • besrabasant
    besrabasant over 6 years
  • Tao
    Tao over 6 years
    I guess the problem is that the merged object isn't an enum anymore for TypeScript although it behaves the same. As a workaraound you can write typeof AnimalTypes.Bats which will resolve to Mammals.Bats. Maybe someone else knows a better way.
  • besrabasant
    besrabasant almost 6 years
    haven't really thought this angle... will try it.
  • MarsRobot
    MarsRobot about 5 years
    add type Animals = Mammals | Reptiles; from @Porkishka answer and it works and passes type checks
  • Ates Goral
    Ates Goral almost 5 years
    What a comprehensive answer!
  • lolelo
    lolelo over 4 years
    I can't use the nested enum with a switch
  • Théry Fouchter
    Théry Fouchter over 4 years
    @lolelo You can do it in a little hackish way with typescript. Here is a sample (also in edited answer):pastiebin.com/5d681d2cde9cc
  • Shashank Vivek
    Shashank Vivek over 4 years
    How did you put those "RED" cross and "GREEN" ticks ? :D
  • Théry Fouchter
    Théry Fouchter over 4 years
    @ShashankVivek basically copy/pasting emojis (symbols section). They are a great ressource for written explanations :)
  • towc
    towc over 4 years
    how would I go about exporting Animals now?
  • maboesanman
    maboesanman about 4 years
    for example 1 and 2, instead of export type Animals = typeof Animals; you should be doing export type Animals = Mammals | Reptiles; so that mammal is assignable to animal
  • Arnaud Bertrand
    Arnaud Bertrand about 4 years
    if you export both the variable and the type you should not have any issues
  • dave0688
    dave0688 almost 4 years
    The problem with this approach is, that you cannot select any value from it. Error: Animals then. Animals only refers to a type, but is being used as a value here
  • Nicolas
    Nicolas about 3 years
    Thanks Théry Fouchter for the wonderful answer and @maboesanman for the great addition. I was stuck on this for quite a while.
  • Connor Low
    Connor Low almost 3 years
    Thorough, but as @maboesanman pointed out, you got the type merge wrong. Then you can do const a: Animals = Animals.Bats like a normal enum.