Nine terrible ways to use TypeScript enums, and one good way.
TypeScript enums get a lot of hate. And not for an unjustified reason: they have so many potential foot-guns.
But — my position is, they still belong in the language rather than out. Like the proverbial doctor advising the patient whose arm hurts when he moves it like that, I’ll argue: just don’t move it like that.
I do believe that a future major version of TypeScript should remove or obviate a lot of these anti-patterns, though.
1. Don’t rely on implicit values
One way to write an enum in Typescript is:
enum Suit {
Hearts,
Diamonds,
Clubs,
Spades
}
This transpiles to something similar to:
const Suit = {
Hearts: 0,
Diamonds: 1,
Clubs: 2,
Spades: 3
}
Obviously, this is awful. Re-order the enum keys, or add new enum keys, and you risk changing the automatically generated enum values.
2. Don’t define explicit numeric values
When you do, you end up with a weird result:
enum Suit {
Hearts = 0,
Diamonds = 1,
Clubs = 2,
Spades = 3
}
Transpiles to:
const Suit = {
Hearts: 0,
Diamonds: 1,
Clubs: 2,
Spades: 3,
0: 'Hearts',
1: 'Diamonds',
2: 'Clubs',
3: 'Spades'
}
For whatever reason, this does not happen if you use string enum values.
I believe this is done to allow you to do a reverse lookup of the key, more easily, for example:
const heartsKey = Suit[Suit.Hearts];
But this brings us to the next tip:
3. Don’t do reverse lookups of enum keys
There isn’t really a good reason to do things like this.
const heartsKey = Suit[Suit.Hearts];
The values of an enum should be significant to your code. The key names are an implementation detail, and only really serve to make your code more readable.
If you need a descriptive name, just define a new mapping of enum value to name.
4. Don’t use numeric enum values in functions
Yeah, these things really suck.
enum Suit {
Hearts = 0,
Diamonds = 1,
Clubs = 2,
Spades = 3
}
function logSuit(suit : Suit) {
console.log(suit);
}
logSuit(1337);
This doesn’t raise a type error. So really what’s the point of even having the enum at this point?
5. Don’t use literal values when the type is enum
One of the common arguments against enums is: “I can’t pass a literal value, even if it belongs to the enum”
enum Suit {
Hearts = 'hearts',
Diamonds = 'diamonds',
Clubs = 'clubs',
Spades = 'spades'
}
function logSuit(suit : Suit) {
console.log(suit);
}
logSuit('hearts');
// Argument of type '"hearts"' is not assignable to parameter of type 'Suit'.(2345)
To me, this is actually a feature, not a bug. TypeScript treats enums as ‘nominal’, meaning it’s not enough to just use a value with the correct type, you actually have to use the specific named type.
I’m not sure why I would actually want arbitrary magic strings floating around my codebase like ‘hearts’. Importing the enum is a lot cleaner, and gives me an obvious record of everywhere that particular enum is used in code, that I can use to search by reference.
6. Don’t use an object instead of an enum
This is common advice, but I kind of hate it.
We can replace the enum with a literal object, with as const
to preserve the literal value types, so they’re not upcast to string
:
const Suit = {
Hearts: 'hearts',
Diamonds: 'diamonds',
Clubs: 'clubs',
Spades: 'spades'
} as const
Then we can extract an enum-esque type by inferring a union of the values of this object:
type SuitEnum = (typeof Suit)[keyof typeof Suit]
I mean sure, this works, but:
- It suffers from the “Don’t use literal values when the type is enum” problem
- We have to remember to mark
Suit
withas const
. If we forget to do this, our code is implicitly less type safe:SuitEnum
is juststring
. There are no warnings to remind you to add theas const
- Creating
SuitEnum
results in quite a verbose bit of code, which is easy to mess up, and whichenum
already does in a nice pithy way
So to me this pattern just creates more foot-guns than it solves.
7. Don’t use a string literal union instead of an enum
Other people recommend a pattern like this:
type Suit = 'hearts' | 'diamonds' | 'clubs' | 'spades';
This is fine, except:
- You end up with magic strings everywhere in your code, which often makes me nervous that someone has just defined the type as ‘string’
- You have no enum to actually enumerate over; for example if you want to write a schema (e.g. a zod schema) at an api boundary, you don’t have a runtime value to use to do this.
8. Don’t use const enums
I mean eh… const enums are kinda fine.
const enum Suit {
Hearts = 'hearts',
Diamonds = 'diamonds',
Clubs = 'clubs',
Spades = 'spades'
}
function logSuit(suit : Suit) {
console.log(suit);
}
logSuit(Suit.Clubs);
The nice thing about this is the Suit
enum is completely erased at compile time, meaning there’s no weird object hanging around. This is what we get:
function logSuit(suit) {
console.log(suit);
}
logSuit("clubs");
The annoying thing about this is the Suit
enum is completely erased at compile time. This sucks if you actually like having the full enum when you need it.
For example, if you want to write a validator that takes a value from an untrusted source — say as part of a json payload passed into an http api — and checks if that value conforms to an enum type. At that point, you’re going to need the original Suit
object, and with a const enum
you’ve lost it.
9. Don’t declaration-merge enums
Mutable types in TypeScript are kind of nasty whether we’re talking enums or not.
enum Suit {
Hearts = 'hearts',
Diamonds = 'diamonds',
Clubs = 'clubs',
}
enum Suit {
Spades = 'spades'
}
Just… don’t do this. There’s no need.
The one “good” way
- Just use
enum
- Always provide explicit values
- Make sure the values are strings
enum Suit {
Hearts = 'hearts',
Diamonds = 'diamonds',
Clubs = 'clubs',
Spades = 'spades'
}
Yes, this is an artificial constraint. But it’s better than the other alternative artificial constraints you need to use to get around it.
The future
Hopefully a future major version of TypeScript will make something like this the ‘one true way’ to write enums, and also make numeric enum values behave sensibly.
They could even do this with a "saneEnums": true
setting in tsconfig.json
I’d also be totally open to some approach that made using ‘objects as enums’ less cumbersome. Something like:
const Suit = {
Hearts: 'hearts',
Diamonds: 'diamonds',
Clubs: 'clubs',
Spades: 'spades'
};
function logSuit(suit: valueof Suit) {
console.log(suit);
}
But until then, I’ll be sticking with enums.