Why use enums in TypeScript?
Enums let you define named constant sets that TypeScript validates at compile time. Unlike loose strings, enums prevent typos, enable safe refactoring, and autocomplete in your IDE. They're ideal for statuses, roles, content types, and any value with limited, predefined options.
A common mistake is using numeric enums by default (enum Direction { Up, Down }). This generates values 0, 1, 2... which are unreadable in logs and databases. Always prefer string enums with explicit values: enum Direction { Up = 'up', Down = 'down' }. This makes your code more debuggable and compatible with REST APIs.
Another benefit: TypeScript allows using enums as types. If you define type Status = UserStatus.ACTIVE | UserStatus.PENDING, the compiler validates you only use those two specific values, giving you granular type safety without repeating strings.
Naming conventions for enums
Enum names should be singular and PascalCase: UserRole, OrderStatus, PaymentMethod. Values inside go in SCREAMING_SNAKE_CASE (majority convention) or camelCase depending on your team, but stay consistent. String values should be lowercase with underscores: PENDING = 'pending', IN_PROGRESS = 'in_progress'.
Avoid redundant prefixes. If your enum is called UserStatus, there's no need for values like USER_ACTIVE, USER_PENDING. Status.ACTIVE is clear in context. This convention keeps code clean and reduces visual noise.
For public APIs or persisted configurations, string values are part of your contract. Once ACTIVE = 'active' is in production, changing that string breaks integrations. Choose descriptive, stable values from the start—not cryptic codes like A, P, C that you'll regret later.
When to use const enum vs regular enum
Const enums (const enum Color { Red = 'red' }) are completely erased during compilation: TypeScript replaces Color.Red with 'red' inline. This reduces bundle size but you lose the ability to iterate over the enum at runtime or do reverse mapping.
Use const enum only for internal app values where you know you won't need reflection or iteration. For library exports, config enums you need to validate dynamically, or cases where you want Object.values(Status), use regular enums.
A practical case: if you need to generate a select with all enum options, you need iteration: Object.values(PaymentMethod).map(method => option). That doesn't work with const enum. For performance-critical internal code (e.g., a game loop with thousands of comparisons per second), const enum can save microseconds.
Integrating enums with validators and APIs
TypeScript enums don't exist in JavaScript runtime, so you need explicit validation at boundaries (APIs, forms, DB). With Zod you can do: z.nativeEnum(UserStatus) or z.enum(['active', 'pending', ...]). This validates the received string is a legal enum value.
For databases, store the string value, not the numeric index. If your enum evolves (you add a value in the middle), indexes shift and corrupt existing data. With strings, you can add Status.ARCHIVED without breaking records that have 'active' or 'pending'.
When serializing to JSON, string enums serialize directly as strings, which is perfect. If you need to document an API (OpenAPI/Swagger), enums appear as fields with allowed values, generating automatic validation and better documentation for API consumers.