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.