Alex KlausFull Stack Developer  |  Architect  |  Scrum Master
Wonderland of TypeScript enums
01 August 2019

TypeScript enums

Coming from high-level programming languages to TypeScript, a mere mortal dev may naively use an enum for a predefined list of numeric or textual values. Because really, how could it possibly be different in TypeScript?.. Unfortunately, it can and here is a quick run down the Rabbit Hole of TypeScript enums.

Traditional aspects of enums

First of all, TypeScript enums do provide some expected behaviour, like

enum Animal { Dog, Cat, Mouse }

will have the auto-incrementing behaviour, where Dog is initialised with 0, Cat with 1, etc. Defining

enum Animal { Dog = 1, Cat, Mouse }

will shift the auto-incremented values for Cat and Mouse by 1.

Of course, there are string enums. Similar concept — enum Animal { Dog = 'Dog' }.

Easy.

Enum members can be calculated, like

enum Animal {
	Dog = 'tail'.length,
	Cat = 2 + 3,
	Mouse = 1 << 1,
	Mammal = getAnimalValue()
}
function getAnimalValue(): Animal { return Animal.Cat; }

Interesting, but still pretty much aligned with devs’ expectations. Though, gotchas start here — having calculated enum members slightly changes its behaviour:

  • an enum member without initialiser wouldn’t work if goes after calculated members,
  • string valued members are not permitted.

But who would use such a mix?..

enum Animal {
	Dog,				// It's 0.
	Cat = getAnimalValue(),
	Mouse,				// fails: Enum member must have initialiser
	Mammal = 'Mammal'	// fails: Mixture of computed and string values is not permitted
}

Down The Rabbit Hole

Const enum

The way you define the enum matters. Adding the key word const in front of the enum doesn’t generate the enum definition in JavaScript and replaces all the usages to the corresponding constants.

// This has no JavaScript footprint
const enum Animal {
	Dog,
	Cat,
	Mouse
}
// JavaScript will be: var animal = 0;
let animal = Animal.Dog;

And while this behaviour is like using constants of other types, you should also keep in mind a limitation of not using computed members in enum.

Declare enum

As expected, declare enum requires defining the actual code elsewhere. Though, it’s spitting the dummy during the runtime if you refer to a member of this enum.

// No JavaScript footprint
declare enum Animal {
	Dog,
	Cat,
	Mouse
}
Animal.Dog; // Throws runtime error

But declare const enum behaves exactly as const enum and replaces the used members with constants. How transparent is it?

// No JavaScript footprint
declare const enum Animal {
	Dog,
	Cat,
	Mouse
}
// JavaScript will be: var animal = 0;
let animal = Animal.Dog;

Reverse mapping

There is a feature, resolving member’s name by value.

enum Animal {
	Dog
}
let value = Animal.Dog; // returns 0;     name -> value resolution
let name = Animal[0];   // returns 'Dog'; value -> name resolution

Nice, but two gotchas:

  • It works for numeric enum members only.
  • In case of const enum definition, Animal[0] will throw a runtime error.

Here devs start writing some helper methods to resolve enum member’s name by value, because TypeScript doesn’t supply such functionality. These four helper methods from enum-values npm package may help you out.

Accessing the keys

Enums are real objects that exist at runtime. However, instead of the keyof keyword you need to use keyof typeof to get all enum keys as strings.

enum Animal {
	Dog
}
let doggy: keyof typeof Animal = 'Dog';

Thankfully, some type checks are enforced during the compilation, but not all.

const wrong: keyof typeof Animal = 'Robot';			// Compilation error here
const stillWrong = 'Robot' as keyof typeof Animal;	// No compilation error

Bonus enum features

Have you heard about Ambient enums? Check out the official docs if interested to see another corner case.

And what about ”enums as union”? Just a quote from the docs:

When all members in an enum have literal enum values, some special semantics come to play.

Sure, you “love” special semantics. And below there

enum types themselves effectively become a union of each enum member

Yeah… Again, see the docs if interested.

All of that flexibility is quite unexpected from Anders Hejlsberg, the core dev of TypeScript and the author of Turbo Pascal, also the architect of Delphi and C#.

Cognitive dissonance for C# devs

There is one pothole, which full stack C#/TypeScript devs are hitting particular often — passing around enum values expecting to get the keys:

myMethod(animal: Animal) {
	// Got a value here
}
...
myMethod(Animal.Dog);

The mistake can be concealed if the member’s name is the same as its value:

enum Animal {
	Dog = "Dog"
}

And then realising the mistake too late, frantically patching the solution with helper methods to resolve enum member’s name by value before passing it to another module.

Instead of just writing:

myMethod(animal: keyof typeof Animal) {
	// Got a key here
}
...
myMethod('Dog');	// The parameter is checked during the compilation

The last aspects of passing the enum’s key as a string, is not obvious. However, it’s simple, supported by code-completion in modern IDEs and more importantly, type checks are enforced.

When to use enums?

Perhaps, there are only 2 cases for using enums:

  • simple key-value pairs;
  • highly tuned performance.

Otherwise, think about alternatives, like union types (e.g. type Animal = 'Dog' | 'Cat' | 'Mouse') or even classes. The later won’t have high performance, but it might be a win from the maintenance perspective.

Try to avoid the fancy flexibility of enums. You may have a black belt in TypeScript, but other team members may not be at your level, so sooner or later the code will be ruined anyway. Maintainable code must be simple.

Any thoughts? Comment below, on Twitter or join the Reddit discussion.