In the world of TypeScript, developers constantly seek the most efficient and maintainable ways to represent data. Two popular features, unions and enums, often find themselves in comparison. The question, “TypeScript has unions, so are enums redundant?” is frequently debated within the development community. While both offer ways to define a set of possible values, they serve distinct purposes and possess unique advantages. Unions provide flexibility through type combinations, whereas enums offer structure and readability with named constants. This article explores the nuances of each feature, helping you decide when to leverage unions, enums, or even both, for optimal code clarity and robustness. We will delve into their similarities, differences, and real-world use cases to provide a comprehensive understanding.
Understanding TypeScript Enums
Enums, short for enumerations, are a fundamental feature in TypeScript that allow developers to define a set of named constants. These constants represent distinct values within a defined type. Enums enhance code readability by replacing magic numbers or strings with descriptive names. For instance, instead of using the number 1 to represent “Pending,” you can define an enum like enum Status { Pending = 1, Approved, Rejected }. This not only makes the code easier to understand but also reduces the risk of typos and inconsistencies. TypeScript enums can be numeric or string-based, each with its own advantages.
Numeric enums, by default, auto-increment their values starting from zero. You can also explicitly assign values, as shown in the Status example above. String enums, on the other hand, require explicit assignment of string values to each member. This can be beneficial for debugging and logging purposes, as the string values are more informative than numeric indexes. Additionally, TypeScript supports const enums, which are completely removed during compilation, resulting in more performant JavaScript code. Using enums promotes type safety, preventing unintended values from being assigned to variables of the enum type. The TypeScript compiler enforces that variables assigned to an enum type must be one of the defined enum members, improving code reliability.
Consider this example from Microsoftβs official documentation TypeScript Handbook: Imagine a game with different difficulty levels. You could use an enum to represent these levels: enum Difficulty { Easy, Medium, Hard }. Now, instead of using the integers 0, 1, and 2, you can use Difficulty.Easy, Difficulty.Medium, and Difficulty.Hard, making the code more self-documenting. This illustrates how enums enhance code maintainability and reduce the cognitive load on developers.
Exploring TypeScript Unions
Unions, another powerful feature in TypeScript, enable a variable to hold values of different types. This provides flexibility when dealing with data that can exist in multiple forms. A union type is defined using the pipe (|) symbol to separate the possible types. For example, type Result = string | number; indicates that a variable of type Result can hold either a string or a number. Unions are particularly useful when handling API responses, where a function can either return a successful result or an error message.
TypeScript’s type checker intelligently narrows down the type of a union variable based on the context. This is known as “discriminated unions,” where a common property among the union members is used to determine the specific type at runtime. For instance, consider a scenario where you have different shapes, each with a kind property indicating the shape type: type Circle = { kind: “circle”; radius: number; }; type Square = { kind: “square”; sideLength: number; }; type Shape = Circle | Square;. You can then use a switch statement to handle each shape type based on its kind property, ensuring type safety and preventing runtime errors. Unions offer a way to model complex data structures with varying properties, improving code expressiveness and reducing the need for type assertions.
For example, suppose you’re building a function that processes user input, which can be either a string representing the user’s name or null if the user hasn’t provided their name. You can define the function’s parameter type as string | null. When processing the input, you can use a type guard to check if the input is a string before performing string-specific operations. This approach ensures that your code handles both cases correctly, preventing potential errors. As stated by Dr. Axel Rauschmayer in his book “Effective TypeScript” Exploring TypeScript, “Unions are a powerful tool for expressing complex type relationships in TypeScript.” This underscores the versatility and importance of unions in modern TypeScript development.
Unions vs. Enums: A Detailed Comparison
Now that we’ve explored both unions and enums, let’s compare them directly to understand when to use each. The core difference lies in their intended purpose: enums are designed to represent a fixed set of named constants, while unions allow a variable to hold values of different types. While there is overlap in functionality, each offers unique advantages. Enums provide better readability and type safety when dealing with a predefined set of values, whereas unions offer more flexibility when handling data that can exist in multiple forms. Choosing the right tool depends on the specific requirements of your project and the nature of the data you’re working with.
Consider the following key differences:
- Readability: Enums enhance code readability by replacing magic numbers or strings with descriptive names.
- Type Safety: Both offer type safety, but enums enforce a strict set of predefined values.
Here’s another way to look at it: - Unions are more flexible when dealing with data that can change over time.
- Enums are better suited for representing fixed, well-defined sets of values.
In scenarios where you have a fixed set of possible values with meaningful names, enums are generally the preferred choice. For example, representing the days of the week or the status of an order. However, when you need to handle data that can be of different types or have varying properties, unions provide greater flexibility. For instance, representing the result of an API call, which can be either a successful response or an error message.
Featured Snippet Optimized Paragraph: The key distinction is that enums define a type with a fixed set of named values, ideal for representing states or categories, while unions define a type that can hold values of different types, suitable for situations where a variable might be one thing or another. Choosing between them depends on whether you need a closed set of named constants or a flexible type that can accommodate multiple data types.
Practical Examples and Use Cases
To further illustrate the differences and use cases, let’s consider some practical examples. Imagine you’re building a user interface with different button states: enum ButtonState { Enabled, Disabled, Loading }. Using an enum here provides clear and descriptive names for each state, making the code more readable and maintainable. Now, suppose you’re fetching data from an API, which can either return a successful result or an error message: type APIResult = { success: true; data: any; } | { success: false; error: string; }. A union type is perfect for representing this scenario, as it allows the variable to hold either a success object or an error object.
Another example involves representing different types of geometric shapes. You could use a discriminated union to define the shape type and its properties: type Circle = { kind: “circle”; radius: number; }; type Square = { kind: “square”; sideLength: number; }; type Shape = Circle | Square;. This allows you to write code that handles each shape type differently based on its kind property. Conversely, if you were representing a set of predefined colors, an enum would be a more appropriate choice: enum Color { Red, Green, Blue }. These examples highlight the importance of choosing the right tool for the job, based on the specific requirements of your project.
Let’s consider a more complex scenario: validating user input. Suppose you have a function that validates an email address or a phone number. You could use a union type to represent the input, which can be either a string representing an email or a number representing a phone number. The function can then use type guards to validate the input based on its type. This demonstrates how unions can be used to handle complex data validation scenarios. For more insights into using unions with type guards, check out this article on SitePoint TypeScript Union Types: A Practical Guide.
Best Practices and Recommendations
When deciding between unions and enums, consider the following best practices: Use enums when you have a fixed set of named constants that represent distinct values. They enhance code readability and type safety. Use unions when you need a variable to hold values of different types or when dealing with data that can exist in multiple forms. They provide flexibility and expressiveness. Avoid using enums when the set of possible values is likely to change frequently. Unions are better suited for representing data that can evolve over time. Prefer discriminated unions when working with complex data structures that have varying properties. This ensures type safety and prevents runtime errors. In some cases, you might even find that a combination of unions and enums is the most appropriate solution.
Consider the following steps to help guide your decision:
- Identify the possible values or types that a variable can hold.
- Determine if the set of values is fixed and well-defined or if it can change over time.
- Evaluate the readability and maintainability of the code using both unions and enums.
- Choose the option that best represents the data and promotes code clarity.
Ultimately, the best choice depends on the specific context and requirements of your project. By carefully considering the advantages and disadvantages of each feature, you can make informed decisions that lead to more robust, maintainable, and expressive TypeScript code. Remember, understanding the nuances of each feature is key to writing effective TypeScript code.
- When should I use enums over unions?
- Use enums when you have a fixed, well-defined set of named constants. They improve readability and type safety for representing states or categories.
- Are unions more flexible than enums?
- Yes, unions are more flexible as they allow a variable to hold values of different types, making them suitable for scenarios where data can exist in multiple forms.
- Can I use both unions and enums together?
- Yes, you can combine unions and enums to represent complex data structures. For example, an enum could define a status, and a union could allow a variable to be either the status enum or an error object.
Question & Answer :
Ever since TypeScript introduced unions types, I wonder if there is any reason to declare an enum type. Consider the following enum type declaration:
enum X { A, B, C } var x: X = X.A;
and a similar union type declaration:
type X: "A" | "B" | "C" var x: X = "A";
If they basically serve the same purpose, and unions are more powerful and expressive, then why are enums necessary?
With the recent versions of TypeScript, it is easy to declare iterable union types. Therefore, you should prefer union types to enums.
How to declare iterable union types
const permissions = ['read', 'write', 'execute'] as const; type Permission = typeof permissions[number]; // 'read' | 'write' | 'execute' // you can iterate over permissions for (const permission of permissions) { // do something }
When the actual values of the union type do not describe theirselves very well, you can name them as you do with enums.
// when you use enum enum Permission { Read = 'r', Write = 'w', Execute = 'x' } // union type equivalent const Permission = { Read: 'r', Write: 'w', Execute: 'x' } as const; type Permission = typeof Permission[keyof typeof Permission]; // 'r' | 'w' | 'x' // of course it's quite easy to iterate over for (const permission of Object.values(Permission)) { // do something }
Do not miss as const assertion which plays the crucial role in these patterns.
Why it is not good to use enums?
- Non-const enums do not fit to the concept “a typed superset of JavaScript”
I think this concept is one of the crucial reasons why TypeScript has become so popular among other altJS languages. Non-const enums violate the concept by emitting JavaScript objects that live in runtime with a syntax that is not compatible with JavaScript.
- Const enums have some pitfalls
Const enums cannot be transpiled with Babel
There are currently two workarounds for this issue: to get rid of const enums manually or with plugin babel-plugin-const-enum.
Declaring const enums in an ambient context can be problematic
Ambient const enums are not allowed when the --isolatedModules flag is provided. A TypeScript team member says that "const enum on DT really does not make sense" (DT refers to DefinitelyTyped) and “You should use a union type of literals (string or number) instead” of const enums in ambient context.
Const enums under --isolatedModules flag behave strangely even outside an ambient context
I was surprised to read this comment on GitHub and confirmed that the behavior is still true with TypeScript 3.8.2.
- Numeric enums are not type safe
You can assign any number to numeric enums.
enum ZeroOrOne { Zero = 0, One = 1 } const zeroOrOne: ZeroOrOne = 2; // no error!!
- Declaration of string enums can be redundant
We sometimes see this kind of string enums:
enum Day { Sunday = 'Sunday', Monday = 'Monday', Tuesday = 'Tuesday', Wednesday = 'Wednesday', Thursday = 'Thursday', Friday = 'Friday', Saturday = 'Saturday' }
I have to admit that there is an enum feature that cannot be achieved by union types
Even if it is obvious from the context that the string value is included in the enum, you cannot assign it to the enum.
enum StringEnum { Foo = 'foo' } const foo1: StringEnum = StringEnum.Foo; // no error const foo2: StringEnum = 'foo'; // error!!
This unifies the style of enum value assignment throughout the code by eliminating the use of string values or string literals. This behavior is not consistent with how TypeScript type system behaves in the other places and is kind of surprising and some people who thought this should be fixed raised issues (this and this), in which it is repeatedly mentioned that the intent of string enums is to provide “opaque” string types: i.e. they can be changed without modifying consumers.
enum Weekend { Saturday = 'Saturday', Sunday = 'Sunday' } // As this style is forced, you can change the value of // Weekend.Saturday to 'Sat' without modifying consumers const weekend: Weekend = Weekend.Saturday;
Note that this “opaqueness” is not perfect as the assignment of enum values to string literal types is not limited.
enum Weekend { Saturday = 'Saturday', Sunday = 'Sunday' } // The change of the value of Weekend.Saturday to 'Sat' // results in a compilation error const saturday: 'Saturday' = Weekend.Saturday;
If you think this “opaque” feature is so valuable that you can accept all the drawbacks I described above in exchange for it, you cannot abandon string enums.
How to eliminate enums from your codebase
With the no-restricted-syntax rule of ESLint, as described.