Should You Use Enums or Union Types in Typescript?

TypeScript

While using TypeScript, there’s something that we need to do quite often: define the type of a variable as one of multiple possibilities. For instance, let’s say that the status of a button can be “hidden”, “enabled”, or “disabled”.

How do you do that with TypeScript? You might know that there at least 2 ways:

// With string enums
export enum ButtonStatus {
  HIDDEN = 'HIDDEN',
  ENABLED = 'ENABLED',
  DISABLED = 'DISABLED',
};

// With union types of string literals
type ButtonStatus = 'HIDDEN' | 'ENABLED' | 'DISABLED';

There are also basic enums (without the = ‘HIDDEN’ part) but we won’t cover these because the drawbacks are too obvious to make it a fair fight: for instance, when debugging you get an obscure number for the value instead of something you can use.

And there are probably a million other solutions, but let’s compare the two outlined above.

Declaring and accessing

As seen above, declaring a union type is terser.

To use it, a modern editor like VS code will give autocompletion in both cases. But enums need to be exported and imported, while you get union types completion immediately.

Autocomplete is instant in VS code with union types

2 points for union types!

Extending

With union types:

type DynamicButtonStatus = ButtonStatus | 'LOADING';

And with enums? It’s not really possible ☹️

1 point for union types!

Using with switch statements

// Union types
switch (key) {
  case 'HIDDEN': return ...;
  case 'ENABLED': return ...;
  case 'DISABLED': return ...;
  // Notice that there's no default case
}

// Enums
switch (key) {
  case ButtonStatus.HIDDEN: return ...;
  case ButtonStatus.ENABLED: return ...;
  case ButtonStatus.DISABLED: return ...;
  default: return ...;
}

With enums, the default case is compulsory, TypeScript does not detect that all cases are already covered. This is a bigger problem than it looks: If you change the enum to add a status, you might forget to update you switch statement.

On the other hand, with a union type, you will get a TypeError and be reminded to handle the new case.

2 points for union types!

Iterating

This is not the most common thing to do, but we might want to iterate over our button statuses:

// Union types
// ❌ Can't iterate over a type, defining an array is needed
const ButtonStatus = ['HIDDEN', 'ENABLED', 'DISABLED'] as const;
// Notice that the array and type can have the same name if you want
type ButtonStatus = typeof buttonStatuses[number];

for(const status of buttonStatus) {
  // ✅ the type of status here is ButtonStatus
  // ✅ always iterate in order
}

// String enums
// ✅ Just do it
for(const status in ButtonStatus) {
  // ❌ The type of status here is string
  // ❌ iteration order not guaranteed with "in"
}

This is a tough one, let’s give… 1 point to enums!

Renaming

At some point, we might change our mind and say that we want our ‘ENABLED’ status to be called ‘PRESSABLE’ instead.

With enums, this is just a matter of right click > Rename symbol

With union types, you will have to fallback to Find and replace in whole project, which is a bit less precise…

2 points for enums!

Misc

  • There’s only one way to use union types, while with enums you can use ButtonStatus.HIDDEN or directly 'HIDDEN' without the compiler complaining.
  • If code uniformity matters to you, it’s easy to ban enums with an ESlint rule, while banning “string literal union types” is much more complicated since union types are everywhere in TypeScript code.
  • Enums are compiled to some obscure code, instead of being just removed by the TypeScript compiler. They are one of the few pieces of TS syntax that behave this way. Though that’s rarely a real world problem, it slightly increases bundle size and debugging complexity, and slightly decreases performance.

1 bonus point for union types!

Conclusion

In our perfectly fair comparison, union types get 6 points and enums get 3 points!

This means using union types by default, and maybe making an exception if you like to rename things all the time or your tooling doesn’t provide live typecheking and code completion.

But perhaps more importantly, keep in mind that this topic is a bit like tabs vs spaces: while it’s true that there are arguments for both, which one you’re using doesn’t matter that much in the end.

My hope with this article is that you can understand the trade-offs, pick one of the two options for your project, stick with it, and never have to talk about it again :).