TypeScript Enums Best Practices

Use enums effectively for type-safe constants and avoid common pitfalls.

TL;DR

  1. 01Use string enums for better debugging and serialization.
  2. 02Use const enums to avoid runtime overhead.
  3. 03Prefer union types over enums for simpler type definitions.

Tips

  1. 01Use union types with <code>as const</code> instead of enums for most cases — they're simpler and don't have the pitfalls of enum reverse mapping.

Warnings

  1. 01Numeric enums can cause subtle bugs due to reverse mapping — prefer string enums or union types to avoid confusion.

String Enums

  • Create enums with string values for clarity.
    enum Status {
      Active = "active",
      Inactive = "inactive",
      Pending = "pending"
    }
    
    const userStatus: Status = Status.Active;
  • String enums are self-documenting and easier to debug.
    // Logs "active" instead of "0" — readable in console and logs
    console.log(Status.Active); // "active"
  • Values are explicit and match database/API values.
    enum Role {
      Admin = "admin",
      User = "user",
      Guest = "guest"
    }
    
    function checkRole(role: Role) {
      if (role === Role.Admin) {
        // user is admin
      }
    }
  • Use string enums in switch statements for exhaustive type checking.
    function describe(s: Status): string {
      switch (s) {
        case Status.Active:   return "Active";
        case Status.Inactive: return "Inactive";
        case Status.Pending:  return "Pending";
      }
    }
  • Iterate string enum values safely with Object.values.
    const values = Object.values(Status);
    // ["active", "inactive", "pending"]

Numeric Enums

  • Create enums with numeric values for flexibility.
    enum Direction {
      Up = 0,
      Right = 1,
      Down = 2,
      Left = 3
    }
    
    const dir: Direction = Direction.Up;
  • Numeric enums support reverse mapping.
    enum Status {
      Active = 1,
      Inactive = 2
    }
    
    console.log(Status[1]);  // "Active"
    console.log(Status.Active);  // 1
  • Auto-incrementing values reduce boilerplate.
    enum Level {
      Low,      // 0
      Medium,   // 1
      High      // 2
    }
  • Start numeric enums at 1 to avoid falsy zero bugs.
    enum Priority {
      Low = 1,  // avoids if (priority) being false for Low
      Medium,   // 2
      High      // 3
    }
  • Use bit flags with numeric enums for combined permissions.
    enum Permission {
      None  = 0,
      Read  = 1 << 0, // 1
      Write = 1 << 1, // 2
      Admin = 1 << 2  // 4
    }
    
    const canEdit = Permission.Read | Permission.Write; // 3

Const Enums

  • Use const for enums that are inlined at compile time.
    const enum Color {
      Red = "red",
      Green = "green",
      Blue = "blue"
    }
    
    const myColor: Color = Color.Red;
  • Const enums don't generate runtime code.
    const enum Direction { Up, Down, Left, Right }
    const d = Direction.Up;
    // Compiles to: const d = 0; — no enum object in bundle
  • Smaller bundle size and faster performance.
    // Compiles to:
    const myColor = "red";
  • Only string values work reliably with const enums.
    // Use string values to stay safe across modules and bundlers
    const enum HttpMethod { Get = "GET", Post = "POST" }
  • Avoid const enums in published library declaration files.
    // Library consumers can't use const enums from .d.ts files
    // Use a regular enum or union type instead
    export type HttpMethod = "GET" | "POST" | "PUT";

Heterogeneous Enums

  • Mix string and numeric values in a single enum.
    enum Mixed {
      No = 0,
      Yes = "YES"
    }
  • Avoid heterogeneous enums — they are hard to reason about.
    // Confusing: mixing types makes iteration and reverse lookup unpredictable
    enum Bad { A = 0, B = "b" }
  • Use separate enums or union types instead for clarity.
    // Better: separate enums
    enum NumericStatus {
      Inactive = 0,
      Active = 1
    }
    
    enum StringStatus {
      Inactive = "inactive",
      Active = "active"
    }
  • Keep all enum members the same type to enable consistent iteration.
    // Safe: all strings — Object.values returns only the values
    enum Color { Red = "red", Green = "green", Blue = "blue" }
    const colors = Object.values(Color); // ["red", "green", "blue"]
  • TypeScript emits a warning for most heterogeneous patterns.
    // Prefer string or numeric uniformly to avoid compiler warnings
    enum Uniform { A = "a", B = "b", C = "c" } // clean

Union Types Alternative

  • Use union types instead of enums for simpler definitions.
    type Status = "active" | "inactive" | "pending";
    
    const status: Status = "active";
  • Union types are simpler and don't generate runtime code.
    // No object created — pure compile-time type
    type Role = "admin" | "user" | "guest";
  • Union types are more flexible for type checking and inference.
    type Role = "admin" | "user" | "guest";
    
    function hasAccess(role: Role): boolean {
      return role === "admin" || role === "user";
    }
  • Use as const to create both a type and runtime constants.
    const STATUSES = {
      Active: "active",
      Inactive: "inactive"
    } as const;
    
    type Status = typeof STATUSES[keyof typeof STATUSES];
    // Status = "active" | "inactive"
  • Prefer union types when values are already plain strings in your codebase.
    // API returns "GET" | "POST" directly — no enum needed
    type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
    
    function request(method: HttpMethod, url: string) { }

FAQ