public-rules › TypeScript techs/typescript

Prefer literal unions over enums

MEDIUM1.0.0
When to apply Before writing, changing, or reviewing a TypeScript type for a fixed set of values, such as roles, statuses, or modes, or code that declares an enum.

Represent a fixed set of values with a union of string literals. When code also needs the values at runtime, derive the union from an as const array or object.

Implementation

  • Use type Role = 'guest' | 'moderator' | 'admin' when only the type is needed.
  • Use const ROLES = ['guest', 'moderator', 'admin'] as const and type Role = (typeof ROLES)[number] when code iterates over the values.
  • Use an as const object when names map to different values, such as color names to hex codes.
  • Enable the erasableSyntaxOnly compiler option, or ban TSEnumDeclaration with no-restricted-syntax, to prevent new enums.

Rationale

An enum compiles to a runtime object and is nominal: a plain string 'admin' is not assignable to Role.Admin. Enums are also not erasable syntax, so tools that run TypeScript by stripping types, such as Node's built-in type stripping, reject them; TypeScript 5.8 added erasableSyntaxOnly to flag them. Literal unions have no runtime cost and accept plain values that match.

Examples

Incorrect (counterexample):

enum UserRole {
  Guest = 'guest',
  Admin = 'admin',
}

setRole('admin'); // error: string is not assignable to UserRole

Correct:

const USER_ROLES = ['guest', 'admin'] as const;
type UserRole = (typeof USER_ROLES)[number];

setRole('admin');

for (const role of USER_ROLES) {
  seedRole(role);
}

Validation

Run the type checker with erasableSyntaxOnly, or the lint rule, and check that no enums remain in authored code.

Enums in generated code or third-party types are not a violation.