Enums with Typescript - why it's bad? Possible solutions

Have you ever been told to avoid using enums in TypeScript, but weren't sure why? While they can be useful for defining constant values, enums can actually be quite dangerous. This article will explore the potential problems with enums and suggest safer and more reliable alternatives.

The problem with code generated by enums

One reason to avoid using enums is the extra code they generate at compile time. This can increase the size of the final file and negatively affect the loading speed and performance of the application. Even using constant enums instead of regular enums may not completely solve this problem. This is something to consider, especially if files are shared between the frontend and backend of an application.

The issue with numeric types

Another problem with regular numeric enums that do not set string values is that they are not type-safe. This means that a function that takes user roles can accept any numeric value, which can cause security and performance problems. For example, a user with a numeric value of 10 could potentially access a role they shouldn't be able to.

enum Roles {
  Producer,
  Consumer,
}

function visible(role: Roles): boolean {
  return role === Roles.Producer
}
console.log(visible(Roles.Producer)) // true
console.log(visible(10)) // putting number is allowed here
Problem with dangerous enums - numbers are valid

The problem with named types

Enums are a named type, meaning they only accept certain values and cannot be passed to functions or objects that expect a string enumeration. This can lead to compatibility issues and requires careful consideration when using and designing enums.

First solution - assign values to enum

enum Roles {
  Producer = 'producer',
  Consumer = 'consumer',
}

function visible(role: Roles): boolean {
  return role === Roles.Producer
}
console.log(visible(Roles.Producer)) // true
console.log(visible(10)) // not allowed which is cool!
console.log(visible('consumer')) // not allowed because its a string - problem with flexibility here
Solution 1 - assign string values to the enum

In this solution, we can assign string values to the enum, such as producer and consumer. This allows us to make our code more predictable and robust. We can also avoid the flexibility problem where passing a string value to the function would not work. The visible() function takes an argument of type Roles, which is the enumeration we defined. We can pass a value of Roles.Producer to the function, and it will return true. However, if we try to pass a value of 10, which is not an option in the enum, we will get an error. This error prevents us from passing invalid data, which can help us catch potential bugs early. Similarly, if we try to pass a string value like consumer, the function will return false because consumer is not an option in the enum - this is not really an elastic approach.

Second solution - use object instead

const ROLES = {
  PRODUCER: "producer",
  CONSUMER: "consumer",
} as const;
type ROLES = typeof ROLES[keyof typeof ROLES];

function visible(role: ROLES): boolean {
  return role === ROLES.PRODUCER
}
console.log(visible(ROLES.PRODUCER)) // true
console.log(visible(10)) // not allowed which is cool!
console.log(visible('producer')) // we can use string which is great! flexibility!
Solution 2 - use an object because it has the best flexibility and secure

Using an object with string values allows for more flexibility than enums because values can be added or removed at runtime without generating additional code. In addition, the use of the "as const" assertion ensures that the values are treated as string literals, providing type safety. However, it's worth noting that this solution may not be suitable for all use cases, especially when the number of values is very large.