TypeScript Enums Best Practices
Use enums effectively for type-safe constants and avoid common pitfalls.
TL;DR
- 01Use string enums for better debugging and serialization.
- 02Use const enums to avoid runtime overhead.
- 03Prefer union types over enums for simpler type definitions.
Tips
- 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
- 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
Use string enums whenever enum values will appear in API payloads, logs, or serialized data — Status.Active staying as "active" at runtime is far easier to trace than 0. String enums also avoid the reverse-mapping behavior that numeric enums generate, keeping the compiled output clean.
A const enum is inlined at compile time — TypeScript replaces every usage with the literal value and emits no runtime object, reducing bundle size. However, they break under isolatedModules (used by Babel, esbuild, and Vite), so avoid them in library code or projects with those bundler setups.
Define an object with as const, then derive the value type using indexed access: const Status = { Active: 'active', Inactive: 'inactive' } as const; type Status = typeof Status[keyof typeof Status];. You get full autocomplete and type narrowing with zero runtime overhead and no reverse-mapping surprises.
TypeScript compiles numeric enums with both forward (Name → number) and reverse (number → Name) entries in the same object, so Object.keys(MyEnum) yields both string names and their numeric counterparts. Filter out entries where Number(key) is not NaN before iterating, or switch to string enums which skip the reverse mapping entirely.
No — heterogeneous enums combine the downsides of both types (reverse mapping from numeric members, inconsistent value shapes) while providing no advantage over two separate typed constants. Split them into distinct string enums or union types based on what each group of values actually represents.