The TypeScript Development Trap You Need to Avoid

Unlocking and Understanding TypeScript Enums: Navigating Pitfalls, Maximizing Their Effectiveness - Your Ultimate Guide to Mastery
Friday, April 28, 2023

Enums are a common feature in TypeScript, but they can be a double-edged sword. While enums can provide a clear and concise way to define a set of values, they can also be inflexible and limited in certain scenarios. In this article, we’ll explore when to avoid enums in TypeScript and when to use them.

When to Avoid Enums in TypeScript

1. When you need more flexibility

Enums can be a bit inflexible when it comes to handling dynamic values. For example, if you need to generate a set of values dynamically based on some condition, an enum may not be the best choice.

// Example of using an enum for handling dynamic values
enum Color {
  Red = '#ff0000',
  Green = '#00ff00',
  Blue = '#0000ff',
}

function getTextColor(isLightMode: boolean): Color {
  return isLightMode ? Color.Black : Color.White;
}

// This code will not work, as Black and White are not valid enum members
getTextColor(true);

In this example, we’re trying to use an enum to represent the color of some text based on a boolean value. However, since the enum only defines three specific values, we can’t use it to represent dynamic values like Black or White. In this scenario, it would be better to use a more flexible construct like a union type:

// Example of using a union type for handling dynamic values
type Color = '#ff0000' | '#00ff00' | '#0000ff' | 'black' | 'white';

function getTextColor(isLightMode: boolean): Color {
  return isLightMode ? 'black' : 'white';
}

By using a union type, we can define a set of values that includes both static and dynamic options.

2. When you need more type safety

Enums can also be a bit limited when it comes to enforcing type safety. For example, it’s possible to assign an integer value to an enum variable that is not a valid enum member:

// Example of assigning an invalid value to an enum variable
enum Fruit {
  Apple,
  Orange,
  Banana,
}

const favoriteFruit: Fruit = 4;

// This code will compile, even though 4 is not a valid Fruit member

In this example, we’re assigning the value 4 to a variable that is supposed to be of type Fruit. However, since 4 is not a valid enum member, this code will compile without any errors. To avoid these types of issues, it’s often better to use more robust constructs like union types or literal types:

// Example of using a union type to enforce type safety
type Fruit = 'apple' | 'orange' | 'banana';

const favoriteFruit: Fruit = 'pineapple';

// This code will not compile, as 'pineapple' is not a valid Fruit member

By using a union type, we can ensure that the variable only contains values from a specific set.

3. When you need more runtime flexibility

Enums are static by nature and cannot be modified at runtime. If you need to add or remove values from an enum, you will need to modify the enum definition and recompile your code.

// Example of trying to modify an enum at runtime
enum Fruit {
  Apple,
  Orange,
  Banana,
}

// This code will not work, as enums are static and cannot be modified at runtime
Fruit.Peach = 3;

Attempting to modify an enum at runtime like in the above example will result in a compilation error. If you anticipate needing to modify values at runtime, you may want to consider alternative options such as using a plain JavaScript object or a Map instead of an enum. This will give you the flexibility to add, remove, or modify values as needed without requiring a recompilation of your code.

4. When you need to serialize or deserialize data

Enums can also be problematic when it comes to serializing and deserializing data. Since enums are represented as integers at runtime, it can be difficult to map them back to their original string values.

// Example of serializing an enum to JSON
enum Fruit {
  Apple,
  Orange,
  Banana,
}

const favoriteFruit: Fruit = Fruit.Apple;

// This will serialize the enum to the integer value 0
const serialized = JSON.stringify(favoriteFruit);

// This will deserialize the JSON to the integer value 0, not the string 'Apple'
const deserialized = JSON.parse(serialized);

In this example, we’re trying to serialize an enum to JSON and then deserialize it back to its original value. However, since enums are represented as integers at runtime, the deserialized value will be an integer instead of a string. To avoid these issues, it’s often better to use a more flexible construct like a string literal union type:

// Example of using a string literal union type for serialization
type Fruit = 'apple' | 'orange' | 'banana';

const favoriteFruit: Fruit = 'apple';

// This will serialize the string value 'apple'
const serialized = JSON.stringify(favoriteFruit);

// This will deserialize the JSON to the string value 'apple'
const deserialized = JSON.parse(serialized);

By using a string literal union type, we can ensure that the serialized and deserialized values remain consistent.

5. When you need to optimize for bundle size

Enums can also add unnecessary bloat to your compiled JavaScript code. Since enums are represented as objects at runtime, they can be larger than other constructs like union types or literal types.

// Example of using an enum with a large number of members
enum Color {
  Red = '#ff0000',
  Green = '#00ff00',
  Blue = '#0000ff',
  Yellow = '#ffff00',
  Cyan = '#00ffff',
  Magenta = '#ff00ff',
  Black = '#000000',
  White = '#ffffff',
  Gray = '#808080',
  Maroon = '#800000',
  Olive = '#808000',
  Navy = '#000080',
  Purple = '#800080',
}

// This code will generate a larger JavaScript object at runtime

In this example, we’re using an enum with a large number of members. However, since enums are represented as objects at runtime, this code will generate a larger JavaScript object than if we had used a more concise construct like a union type or literal type.

When to Use Enums in TypeScript

While there are some scenarios where enums may not be the best choice, there are also many scenarios where they can be very useful. Here are a few scenarios where enums can be a great choice:

1. When you need a fixed set of values

One of the main benefits of enums is that they provide a clear and concise way to define a fixed set of values. This can be useful in scenarios where you need to represent a set of related values:

// Example of using an enum to represent different HTTP methods
enum HttpMethod {
  Get,
  Post,
  Put,
  Delete,
}

function sendRequest(method: HttpMethod, url: string) {
  // ...
}

In this example, we’re using an enum to represent different HTTP methods. By using an enum, we can ensure that only the specific set of methods are allowed.

2. When you need to perform runtime lookups

Enums can also be useful when you need to perform lookups based on a specific value. Since enums are represented as objects at runtime, you can use them to perform runtime lookups with ease:

// Example of using an enum for runtime lookups
enum Currency {
  USD = 'United States Dollar',
  EUR = 'Euro',
  GBP = 'British Pound Sterling',
  JPY = 'Japanese Yen',
}

function getCurrencyName(currency: Currency) {
  return Currency[currency];
}

console.log(getCurrencyName(Currency.USD)); // 'United States Dollar'

In this example, we’re using an enum to perform a runtime lookup for a currency name. By using the Currency enum as an object, we can easily retrieve the name associated with a specific currency value

3. When you need to perform bitwise operations

Enums can also be useful when you need to perform bitwise operations. Since enums are represented as integers at runtime, you can use them to perform bitwise operations with ease:

// Example of using an enum for bitwise operations
enum Permission {
  Read = 1,
  Write = 2,
  Execute = 4,
}

const userPermissions = Permission.Read | Permission.Write;

function hasPermission(permission: Permission) {
  return (userPermissions & permission) === permission;
}

console.log(hasPermission(Permission.Read)); // true
console.log(hasPermission(Permission.Execute)); // false

In this example, we’re using an enum to perform bitwise operations for user permissions. By using the Permission enum as a set of bitwise flags, we can easily check if a user has a specific permission.

4. When you need to map values to other values

Enums can also be useful when you need to map a set of values to other values. Since enums are represented as objects at runtime, you can use them to create mappings between related values:

// Example of using an enum to map values to other values
enum LogLevel {
  Error = 'ERROR',
  Warning = 'WARNING',
  Info = 'INFO',
}

const logLevelMappings = {
  [LogLevel.Error]: 1,
  [LogLevel.Warning]: 2,
  [LogLevel.Info]: 3,
};

function logMessage(level: LogLevel, message: string) {
  const logLevel = logLevelMappings[level];
  console.log(`[${logLevel}] ${level}: ${message}`);
}

logMessage(LogLevel.Error, 'An error occurred'); // '[1] ERROR: An error occurred'

In this example, we’re using an enum to create mappings between log levels and integer values. By using an enum, we can ensure that only the specific set of log levels are allowed and easily create mappings between them.

5. When you need to define a state machine

Enums can also be useful when you need to define a state machine. Since enums provide a clear and concise way to define a fixed set of values, you can use them to define the different states of a state machine:

// Example of using an enum to define a state machine
enum State {
  Idle,
  Loading,
  Loaded,
  Error,
}

function transitionState(currentState: State, nextState: State) {
  // ...
}

transitionState(State.Idle, State.Loading);

In this example, we’re using an enum to define the different states of a state machine. By using an enum, we can ensure that only the specific set of states are allowed and easily transition between them.

Conclusion

Enums can be a useful construct in TypeScript, but they’re not always the best choice. By understanding when to avoid enums and when to use them, you can write better and more maintainable code. In general, it’s best to avoid enums when you need a flexible and extensible solution or when you need to perform complex mappings or computations. On the other hand, enums can be useful when you need a fixed set of values that won’t change often or when you need to perform simple lookups, bitwise operations, or create state machines.

When working with enums, it’s important to keep in mind that they can introduce unnecessary complexity and make your code harder to maintain. If you find yourself using an enum in a way that doesn’t fit its intended use case, consider refactoring your code to use a different construct that better fits your needs. As with any programming construct, the key to using enums effectively is to understand their strengths and weaknesses and use them judiciously. With the guidelines and examples presented in this article, you should now have a better understanding of when to avoid enums and when to use them in TypeScript.

Keep reading

If you liked that one here's another:
Cool Usecases for GitHub

Wanna show support?

If you find my sporadic thoughts and ramblings helpful.
You can buy me a coffee if you feel like it.
It's not necessary but it's always appreciated. The content will always stay free regardless.