Nine terrible ways to use TypeScript enums, and one good way.

Daniel Brain
5 min readDec 15, 2022

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:

  1. It suffers from the “Don’t use literal values when the type is enum” problem
  2. We have to remember to mark Suit with as const. If we forget to do this, our code is implicitly less type safe: SuitEnum is just string. There are no warnings to remind you to add the as const
  3. Creating SuitEnum results in quite a verbose bit of code, which is easy to mess up, and which enum 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, if you like not having an actual enum to reference at run-time, and if you like throwing magic-looking strings everywhere in your code. I don’t like either of those things.

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, and 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.

--

--