Why TypeScript Enums Are Terrible But Union Types Are Great
A deep dive into the pitfalls of TypeScript Enums and the power of Union Types, or why I refactor all my Enums to Union Types.
Introduction: TypeScript Enums vs. Union Types
As developers, we’re constantly looking for ways to improve our codebases and create more maintainable, flexible, and robust software. One of the best ways to achieve this is by leveraging the strengths of different programming paradigms and language features. In this article, we’ll explore why TypeScript Enums are a suboptimal choice for many use cases and how Union Types can offer a superior alternative.
The Multiple Problems with TypeScript Enums
Enums, short for enumerations, are a concept borrowed from languages like C# and Java. They allow developers to define a set of named constants that represent a specific type. At first glance, Enums in TypeScript seem like a great way to improve type safety and code readability. However, they come with a set of drawbacks that may outweigh their benefits in certain scenarios.
Runtime and Compilation Overhead
One of the primary issues with Enums is that they add both runtime and compilation overhead to your codebase. TypeScript Enums are compiled into JavaScript objects with keys for both the names and values of the Enum members. This results in larger output files and additional memory consumption, which can be particularly problematic in performance-critical applications.
enum Direction {
Up,
Down,
Left,
Right,
}
// Compiled JavaScript
"use strict";
var Direction;
(function (Direction) {
Direction[(Direction["Up"] = 0)] = "Up";
Direction[(Direction["Down"] = 1)] = "Down";
Direction[(Direction["Left"] = 2)] = "Left";
Direction[(Direction["Right"] = 3)] = "Right";
})(Direction || (Direction = {}));Limited Flexibility
Enums are also quite rigid, providing limited flexibility when it comes to modifying or extending their values. For example, if you wanted to add a new member to an Enum, you’d need to update every place in your codebase that uses that Enum, which can be time-consuming and error-prone.
Refactoring Challenges
Refactoring Enums can be challenging, especially when it comes to renaming members. Since Enums are compiled into JavaScript objects with keys for both the names and values of the Enum members, renaming an Enum member might break your code in unexpected ways, as it also changes the corresponding value. This can lead to subtle bugs that are difficult to track down.
Embracing Union Types Instead of TypeScript Enums
In contrast to Enums, Union Types are a more flexible, lightweight, and maintainable alternative for representing a fixed set of values. They offer several advantages over Enums, which we’ll discuss below.
Simplicity and Performance
Union Types are simpler than Enums, as they don’t require any additional runtime or compilation overhead. Instead of creating a separate object, Union Types define a new type that is the union of a set of literal types. This makes them more efficient in terms of both memory usage and file size.
type Direction = "Up" | "Down" | "Left" | "Right";Improved Flexibility and Extensibility
Union Types are also more flexible and extensible than Enums. Adding or removing members from a Union Type is as simple as updating the type definition, without the need to modify other parts of your codebase. Additionally, Union Types can be combined using intersections and other type operations, allowing for greater composability and reusability.
type DiagonalDirection = "UpLeft" | "UpRight" | "DownLeft" | "DownRight";
type AllDirections = Direction | DiagonalDirection;Easier Refactoring
Refactoring Union Types is more straightforward than refactoring Enums. Since Union Types don’t generate separate JavaScript objects with keys for both the names and values, renaming a member of a Union Type doesn’t affect the runtime behavior of your code. This makes it easier to refactor your code without introducing subtle bugs.
Comparing IntelliSense for Enums vs. Union Types
Visual Studio Code (VS Code) is a popular code editor that offers powerful IntelliSense features, which can enhance the development experience when working with TypeScript.
Both Enums and Union Types benefit from excellent IntelliSense support in VS Code, with some differences in how the information is presented.
IntelliSense for Enums
First, let’s explore the IntelliSense support for Enums in VS Code, which is good but not great.
When using Enums, VS Code’s IntelliSense provides autocompletion for both the Enum type and its members. This can help speed up the development process by reducing the likelihood of typos and other errors.
enum Direction {
Up,
Down,
Left,
Right,
}
function move(direction: Direction) {
// ...
}
// Autocomplete suggests Enum members when calling the move function
move(Direction.Up);Additionally, hovering over an Enum member will display the value associated with that member, offering further insights into your code.
IntelliSense for Union Types
VS Code’s IntelliSense support for Union Types is even better than it is for Enums, at least if you prefer working with strings like "Up".
When using Union Types, VS Code provides autocompletion for the type members, making it easy to write code that uses the defined set of values.
type Direction = "Up" | "Down" | "Left" | "Right";
function move(direction: Direction) {
// ...
}
// Autocomplete suggests Union Type members when calling the move function
move("Up");Furthermore, hovering over a Union Type member will display the type information, which can help you understand the structure of your code and the valid values for a given type.
Comparing IntelliSense for Enums vs. Union Types
Both Enums and Union Types have excellent support in VS Code’s IntelliSense, providing autocompletion and type information when needed. However, there are some subtle differences in the way this information is presented:
- Member Values: When hovering over an Enum member, VS Code displays the value associated with the member (e.g.,
Direction.Up = 0). In contrast, hovering over a Union Type member shows the type information (e.g.,Direction = "Up" | "Down" | "Left" | "Right"). - Type Composition: Union Types offer greater flexibility in terms of type composition and can be easily combined using intersections and other type operations. IntelliSense support for complex Union Types can provide valuable insights into the structure and valid values of these types, making it easier to work with them.
Personally, I find it much easier to work with Union Types than Enums when it comes to Intellisense. Usually I’m concerned with questions like:
- “What value is being used here?”
- “What values are even valid here?”
- “Why can’t I just pass in a string to this
enum?”
When choosing between Enums and Union Types, consider the trade-offs discussed earlier in this article, such as maintainability, flexibility, and performance, but also consider which IntelliSense experience you prefer. For me, this is the deciding factor in favor of Union Types.
Migrating from Enums to Union Types
If you’re convinced that Union Types are a better fit for your use case, you might be wondering how to migrate your existing codebase from Enums to Union Types. Here’s a step-by-step guide to help you through the process:
1 — Identify the Enums to be replaced
Before making any changes, identify the Enums in your codebase that you’d like to replace with Union Types. Focus on Enums that are causing the most pain in terms of maintainability, flexibility, or performance.
2 — Create the Union Type
Define a new Union Type that represents the same set of values as the Enum. Use string literals to define the members of the Union Type.
// Replace this Enum:
enum Direction {
Up,
Down,
Left,
Right,
}
// With this Union Type:
type Direction = "Up" | "Down" | "Left" | "Right";3 — Update type annotations
Replace all type annotations in your code that reference the Enum with the new Union Type.
// Before
function move(direction: Direction) {
// ...
}
// After
function move(direction: Direction) {
// ...
}4 — Replace Enum member references
Update all references to Enum members with their corresponding string literals.
// Before
move(Direction.Up);
// After
move("Up");5 — Remove the Enum
Once all references to the Enum have been updated, you can safely remove it from your codebase and start enjoying your new Union Types.
By following these steps, you can gradually migrate your codebase from Enums to Union Types, reaping the benefits of improved maintainability, flexibility, and performance along with better IntelliSense hints.
Conclusion: Enums Are Just Too Java For My Taste
Enums come with some drawbacks that may hinder the development and maintenance of your TypeScript codebase, especially if you care about seeing the values you’re actually using (strings or otherwise) at a glance.
Personally I pass around a lot of Tailwind CSS classes as props in my React TSX, and being able to see at a glance what these are is incredibly useful:
import classNames from "classnames"
function BarbieHouse({
bgColor,
}: {
bgColor: "bg-pink-400" | "bg-purple-400"
}) {
return <div className={classNames("h-24 w-24", bgColor)}>🏠</div>
}You can try this particular example in Code Sandbox if you’d like to see my favorite use case for string Union Types in React TypeScript.






