On TypeScript enums

I'm not a huge fan of TypeScripts enums - to me they're a bit of syntax sugar around unions with some added footguns, problems arise when trying to iterate over them which can throw people new to the language off plainly due to how they're transpiled into JavaScript.

Enums explained

In its most basic form an enum is simply:

enum Test {
Hello,
World,
}

So Test.Hello = 0 & Test.World = 1 - this is the worst kind of enum in my opinion as doing Object.keys, Object.values, Object.entries will return seemingly very strange results.

Object.keys(Test); // ["0", "1", "Hello", "World"]
Object.values(Test); // ["Hello", "World", 0, 1]
Object.entries(Test); // [["0", "Hello"], ["1", "World"], ["Hello", 0], ["World", 1]]

Which at first glance might make you wonder why it was done this way? - It's to account for heterogeneous enums.

Taking a look at the JS reveals:

var Test;
(function (Test) {
Test[(Test["Hello"] = 0)] = "Hello";
Test[(Test["World"] = 1)] = "World";
})(Test || (Test = {}));

Shows an immediately invoked function execution with some stuff going on in while indexing Test, lets break that down:

Ok, so now the state of the application looks like this

var Test; // {}
Test[(Test["Hello"] = 0)] = "Hello";
Test[(Test["World"] = 1)] = "World";

Now for the indexing part:

  • Test[Test["Hello"] = 0] = "Hello";, lets eval the indexing first
    • Test["Hello"] is assigned a value of zero, and the assignment is returned, 0
      • Test = { Hello = 0 }
    • Test[0] = "Hello"
      • Test = { Hello = 0, 0 = "Hello" }
    • And same for the next one:
    • Test[Test["World"] = 1] = "World";
      • Test["World"] = 1
      • Test = { Hello = 0, 0 = "Hello", World = 1 }
    • Test[1] = "World"
      • Test = { Hello: 0, 0: "Hello", World: 1, 1: "World" }

And hence we end up with:

var Test = {
  Hello: 0,
  0: "Hello",
  World: 1,
  1: "World"
}

Which fully explains why there are 4 values in our .entries() - a bit of an annoying gotcha & I imagine its taken a few people on a ride around in circles figuring it out.

Better enums

A slightly better way would be to only use string enums as the twice indexing issue is only when using numbers as a value:

enum Test {
Hello = "Foo",
World = "Bar",
}

Object.keys(Test); // ["Hello", "World"]
Object.values(Test); // ["Foo", "Bar"]
Object.entries(Test); // [["Hello", "Foo"], ["World", "Bar"]]

// transpiled...
var Test;
(function (Test) {
Test["Hello"] = "Foo";
Test["World"] = "Bar"; // no duplicates
})(Test || (Test = {}));

Extending enums

It's also not possible to extends enums in the sense of interface X implements Y - there's a couple of work arounds like this one on StackOverflow which work I guess.

enum Color1 {
Red = "Red",
Green = "Green",
}

enum Color2 {
Yellow = "Yellow",
Blue = "Blue",
}

type Colors = Color1 | Color2;

const Colors = { ...Color2, ...Color1 };
this.color = Colors.Red; // Colors.Green or Colors.Yellow or Colors.Blue

But still you have the same issue with repeated indexes - so not a full solution.

An alternative

More recently I've been cozying up to this method of handling enums, though you lose the explicit SomeEnum.Value, assuming everything is typed correctly you'll have no issue with iterating & extending with both strings & numbers.

const Colors1 = ["Red", "Green", 1, 2] as const;
const Colors2 = ["Yellow", "Blue", 3, 4] as const;

// Get values as a union
type Color1 = typeof Colors1[number];
type Color2 = typeof Colors2[number];

// Combine both enums into one
const Colors = [...Colors1, ...Colors2] as const;
type Color = typeof Colors[number];

Colors.forEach((c) => console.log(c)); // "Red", "Green", 1, 2 "Yellow", "Blue", 3, 4;
let x: Color; // "Red" | "Green" | 1 | 2 | "Yellow" | "Blue" | 3, | 4;

thanks for coming to my ted talk.