Enums: When and How to Use Them


Enums: When and How to Use Them#
Enums (enumerations) give you a way to define a set of named constants. They make code more readable by replacing magic numbers and strings with descriptive names. When used appropriately, enums improve code maintainability and reduce errors, but TypeScript also offers compelling alternatives that are sometimes better choices.
Quick Definition: An enum is a special type that defines a collection of related constants. Think of it as a way to say "this value must be one of these specific options."
What Are Enums?
Enums allow you to define a set of named constants that represent distinct values:
enum Direction {
North,
South,
East,
West,
}
// Usage
let heading: Direction = Direction.North;
function move(direction: Direction) {
switch (direction) {
case Direction.North:
console.log("Moving north");
break;
case Direction.South:
console.log("Moving south");
break;
case Direction.East:
console.log("Moving east");
break;
case Direction.West:
console.log("Moving west");
break;
}
}
move(Direction.East); // "Moving east"Enums exist at both compile-time (for type checking) and runtime (as actual JavaScript objects).
Numeric Enums
Numeric enums are the default in TypeScript. Each member gets assigned a numeric value.
Auto-Incrementing Values
By default, enums start at 0 and auto-increment:
enum Status {
Pending, // 0
InProgress, // 1
Completed, // 2
Failed, // 3
}
let currentStatus: Status = Status.Pending;
console.log(currentStatus); // 0
// You can use numeric values
let status: Status = 2; // Status.Completed
console.log(Status[2]); // "Completed" (reverse mapping)Compiled JavaScript:
var Status;
(function (Status) {
Status[(Status["Pending"] = 0)] = "Pending";
Status[(Status["InProgress"] = 1)] = "InProgress";
Status[(Status["Completed"] = 2)] = "Completed";
Status[(Status["Failed"] = 3)] = "Failed";
})(Status || (Status = {}));Custom Numeric Values
You can set specific numeric values:
enum HttpStatus {
OK = 200,
Created = 201,
BadRequest = 400,
Unauthorized = 401,
NotFound = 404,
ServerError = 500,
}
function handleResponse(status: HttpStatus) {
if (status === HttpStatus.OK) {
console.log("Success!");
} else if (status >= 400) {
console.log("Error occurred");
}
}
handleResponse(HttpStatus.NotFound);Partial initialization:
enum Priority {
Low, // 0
Medium = 5,
High, // 6 (continues from previous)
Critical = 10,
}
console.log(Priority.Low); // 0
console.log(Priority.Medium); // 5
console.log(Priority.High); // 6
console.log(Priority.Critical); // 10Reverse Mapping
Numeric enums have reverse mappings—you can get the enum name from its value:
enum Color {
Red = 1,
Green = 2,
Blue = 3,
}
// Forward mapping
let colorValue: number = Color.Red; // 1
// Reverse mapping
let colorName: string = Color[1]; // "Red"
console.log(Color[2]); // "Green"
console.log(Color[3]); // "Blue"
// Useful for debugging
function getColorName(value: number): string {
return Color[value] || "Unknown";
}Note: String enums do NOT have reverse mapping—only numeric enums do.
String Enums
String enums assign string values to each member:
enum Direction {
North = "NORTH",
South = "SOUTH",
East = "EAST",
West = "WEST",
}
let heading: Direction = Direction.North;
console.log(heading); // "NORTH"
// Must use exact enum member, not raw string
heading = "NORTH"; // ✗ Error: Type '"NORTH"' is not assignable to type 'Direction'
heading = Direction.North; // ✓ CorrectWhy Use String Enums
String enums are often better than numeric enums:
1. Self-documenting in logs and debugging:
enum LogLevel {
Debug = "DEBUG",
Info = "INFO",
Warning = "WARNING",
Error = "ERROR",
}
function log(level: LogLevel, message: string) {
console.log(`[${level}] ${message}`);
}
log(LogLevel.Error, "Something went wrong");
// Output: [ERROR] Something went wrong
// Much clearer than [3] Something went wrong2. Serialization-friendly:
enum UserRole {
Admin = "admin",
Editor = "editor",
Viewer = "viewer",
}
interface User {
name: string;
role: UserRole;
}
let user: User = {
name: "Alice",
role: UserRole.Admin,
};
// Serializes to readable JSON
console.log(JSON.stringify(user));
// {"name":"Alice","role":"admin"}3. Refactoring safety:
// If you change the internal value, the enum name stays the same
enum Environment {
Development = "dev", // Was "development", now "dev"
Production = "prod", // Was "production", now "prod"
}
// All code using Environment.Development still worksString Enum Patterns
API endpoints:
enum ApiEndpoint {
Users = "/api/users",
Posts = "/api/posts",
Comments = "/api/comments",
}
async function fetchData(endpoint: ApiEndpoint) {
let response = await fetch(endpoint);
return response.json();
}
fetchData(ApiEndpoint.Users);Event names:
enum EventType {
Click = "click",
MouseMove = "mousemove",
KeyDown = "keydown",
Scroll = "scroll",
}
element.addEventListener(EventType.Click, handler);CSS class names:
enum ButtonVariant {
Primary = "btn-primary",
Secondary = "btn-secondary",
Danger = "btn-danger",
}
function createButton(variant: ButtonVariant) {
return `<button class="${variant}">Click me</button>`;
}Const Enums
Const enums are completely removed during compilation, with their values inlined:
const enum Direction {
North = "NORTH",
South = "SOUTH",
East = "EAST",
West = "WEST",
}
let heading = Direction.North;Compiled JavaScript:
// Enum is completely removed!
let heading = "NORTH"; // Value is inlinedHow Const Enums Work
Regular enum:
enum Color {
Red,
Green,
Blue,
}
let c = Color.Red; // Compiles to: let c = Color.Red;Const enum:
const enum Color {
Red,
Green,
Blue,
}
let c = Color.Red; // Compiles to: let c = 0;Benefits:
- No runtime code generated
- Smaller bundle size
- Better performance (no object lookup)
Limitations:
- No reverse mapping
- Can't iterate over values
- Members must be constant expressions
When to Use Const Enums
Use const enums when:
- Performance is critical
- You want minimal bundle size
- You don't need runtime enum object
- Values are truly constant
Example - Status codes:
const enum HttpStatus {
OK = 200,
NotFound = 404,
ServerError = 500,
}
function handleStatus(status: HttpStatus) {
if (status === HttpStatus.OK) {
// Compiles to: if (status === 200)
console.log("Success");
}
}Trade-off: Const enums give better performance but less flexibility. If you need to iterate over enum values or use them dynamically, use regular enums.
Heterogeneous Enums
Enums can mix string and numeric values (though this is rarely recommended):
enum BooleanLike {
No = 0,
Yes = "YES",
}
// Works but confusing
let value: BooleanLike = BooleanLike.No; // 0
let value2: BooleanLike = BooleanLike.Yes; // "YES"Avoid Heterogeneous Enums: Mixing strings and numbers in the same enum is confusing and error-prone. Stick to all-string or all-numeric enums.
Real-World Use Cases
HTTP Status Codes
enum HttpStatus {
// Success
OK = 200,
Created = 201,
NoContent = 204,
// Client Errors
BadRequest = 400,
Unauthorized = 401,
Forbidden = 403,
NotFound = 404,
// Server Errors
InternalServerError = 500,
ServiceUnavailable = 503,
}
function handleApiResponse(status: HttpStatus, data: any) {
switch (status) {
case HttpStatus.OK:
return { success: true, data };
case HttpStatus.NotFound:
return { success: false, error: "Resource not found" };
case HttpStatus.Unauthorized:
return { success: false, error: "Authentication required" };
default:
return { success: false, error: "Unknown error" };
}
}
// Usage
let response = handleApiResponse(HttpStatus.OK, { user: "Alice" });User Roles and Permissions
enum UserRole {
Admin = "admin",
Moderator = "moderator",
Editor = "editor",
Viewer = "viewer",
}
enum Permission {
Read = "read",
Write = "write",
Delete = "delete",
Manage = "manage",
}
const rolePermissions: Record<UserRole, Permission[]> = {
[UserRole.Admin]: [
Permission.Read,
Permission.Write,
Permission.Delete,
Permission.Manage,
],
[UserRole.Moderator]: [Permission.Read, Permission.Write, Permission.Delete],
[UserRole.Editor]: [Permission.Read, Permission.Write],
[UserRole.Viewer]: [Permission.Read],
};
function hasPermission(role: UserRole, permission: Permission): boolean {
return rolePermissions[role].includes(permission);
}
// Usage
let canDelete = hasPermission(UserRole.Editor, Permission.Delete); // false
let canRead = hasPermission(UserRole.Viewer, Permission.Read); // trueApplication States
enum LoadingState {
Idle = "idle",
Loading = "loading",
Success = "success",
Error = "error",
}
interface DataState<T> {
status: LoadingState;
data: T | null;
error: string | null;
}
function createInitialState<T>(): DataState<T> {
return {
status: LoadingState.Idle,
data: null,
error: null,
};
}
async function fetchData<T>(url: string): Promise<DataState<T>> {
let state: DataState<T> = {
status: LoadingState.Loading,
data: null,
error: null,
};
try {
let response = await fetch(url);
let data = await response.json();
return {
status: LoadingState.Success,
data,
error: null,
};
} catch (error) {
return {
status: LoadingState.Error,
data: null,
error: error.message,
};
}
}API Response Types
enum ResponseType {
JSON = "json",
XML = "xml",
Text = "text",
Blob = "blob",
}
async function apiRequest(url: string, type: ResponseType = ResponseType.JSON) {
let response = await fetch(url);
switch (type) {
case ResponseType.JSON:
return await response.json();
case ResponseType.XML:
return await response.text();
case ResponseType.Text:
return await response.text();
case ResponseType.Blob:
return await response.blob();
}
}
// Usage
let data = await apiRequest("/api/users", ResponseType.JSON);
let image = await apiRequest("/images/logo.png", ResponseType.Blob);Enum Alternatives
TypeScript offers alternatives that are sometimes better than enums.
Union Types
For simple cases, union types are often cleaner:
// Using enum
enum Size {
Small = "small",
Medium = "medium",
Large = "large",
}
// Using union type
type Size = "small" | "medium" | "large";
// Usage is similar
function setSize(size: Size) {
console.log(`Size set to ${size}`);
}
setSize("medium"); // ✓ Works with union type
// setSize(Size.Medium); // Required with enumPros of union types:
- No runtime code
- Can use string literals directly
- Simpler syntax
- Better for serialization
Cons of union types:
- No autocomplete for all values
- Can't iterate over values
- No namespace grouping
Object with as const
Objects with as const provide enum-like behavior:
// Using enum
enum Direction {
North = "NORTH",
South = "SOUTH",
East = "EAST",
West = "WEST",
}
// Using object with as const
const Direction = {
North: "NORTH",
South: "SOUTH",
East: "EAST",
West: "WEST",
} as const;
type Direction = (typeof Direction)[keyof typeof Direction];
// Usage
let heading: Direction = Direction.North;Advantages of objects:
- Works like a regular JavaScript object
- Can iterate over values
- Better tree-shaking in some bundlers
- More flexible (can add methods)
const Status = {
Pending: "pending",
Active: "active",
Completed: "completed",
// Can add helper methods
isActive: (status: string) => status === Status.Active,
} as const;
type Status = (typeof Status)[keyof typeof Status];When to Use Each
Use enums when:
- You need numeric values with reverse mapping
- You want strong namespace grouping
- The values are truly a closed set of options
- Working with external APIs that expect specific values
Use union types when:
- Values are simple strings or numbers
- You want minimal runtime overhead
- Serialization is important
- The type is consumed by external code
Use objects with as const when:
- You need to iterate over values
- You want to add helper methods
- You need a plain JavaScript object
- Working in a codebase that avoids enums
Modern Trend: Many TypeScript developers prefer union types and as const
objects over enums for their simplicity and zero runtime cost.
Common Enum Patterns
1. Enum with helper functions:
enum Status {
Draft = "draft",
Published = "published",
Archived = "archived",
}
namespace Status {
export function isEditable(status: Status): boolean {
return status === Status.Draft;
}
export function canView(status: Status): boolean {
return status !== Status.Archived;
}
}
// Usage
if (Status.isEditable(currentStatus)) {
// Allow editing
}2. Enum iteration:
enum Color {
Red = "red",
Green = "green",
Blue = "blue",
}
// Get all enum values
let colors = Object.values(Color);
console.log(colors); // ["red", "green", "blue"]
// Get all enum keys
let colorNames = Object.keys(Color);
console.log(colorNames); // ["Red", "Green", "Blue"]3. Type-safe enum lookup:
enum Status {
Active = "active",
Inactive = "inactive",
}
function parseStatus(value: string): Status | null {
return Object.values(Status).includes(value as Status)
? (value as Status)
: null;
}
let status = parseStatus("active"); // Status.Active
let invalid = parseStatus("unknown"); // nullBest Practices
1. Use string enums for better debugging:
// ✓ Good - readable in logs
enum LogLevel {
Debug = "DEBUG",
Info = "INFO",
}
// ✗ Avoid - confusing in logs
enum LogLevel {
Debug, // 0
Info, // 1
}2. Use PascalCase for enum names, UPPER_CASE for members:
// ✓ Good
enum HttpStatus {
OK = 200,
NOT_FOUND = 404,
}
// ✗ Inconsistent
enum httpStatus {
ok = 200,
notFound = 404,
}3. Keep enums focused and cohesive:
// ✓ Good - focused
enum UserRole {
Admin = "admin",
User = "user",
}
enum DocumentStatus {
Draft = "draft",
Published = "published",
}
// ✗ Bad - mixing unrelated concepts
enum AppConstants {
AdminRole = "admin",
MaxRetries = 3,
TimeoutMs = 5000,
}4. Consider alternatives before using enums:
// For simple cases, union types are cleaner
type Status = "active" | "inactive" | "pending";
// Instead of
enum Status {
Active = "active",
Inactive = "inactive",
Pending = "pending",
}5. Document enum purpose and values:
/**
* Represents the current state of an order
*/
enum OrderStatus {
/** Order has been placed but not processed */
Pending = "pending",
/** Order is being prepared */
Processing = "processing",
/** Order has been shipped */
Shipped = "shipped",
/** Order has been delivered */
Delivered = "delivered",
}Common Pitfalls
Pitfall 1: Assuming type safety with numeric enums
enum Status {
Pending,
Active,
Completed,
}
let status: Status = 99; // ✓ Compiles! No error
// Numeric enums allow any numberSolution: Use string enums for stricter type safety:
enum Status {
Pending = "pending",
Active = "active",
Completed = "completed",
}
let status: Status = "unknown"; // ✗ Error: Type '"unknown"' is not assignablePitfall 2: Using const enum with external modules
// library.ts
export const enum Direction {
North,
South,
}
// main.ts
import { Direction } from "./library";
let dir = Direction.North; // May not work with some bundlersPitfall 3: Modifying enum objects at runtime
enum Color {
Red,
Green,
Blue,
}
// Don't do this!
Color.Red = 99; // Possible but breaks type safetyPitfall 4: Over-using enums
// ✗ Overkill for two values
enum BinaryChoice {
Yes = "yes",
No = "no",
}
// ✓ Better
type BinaryChoice = "yes" | "no";
// or just use boolean!Next Steps
You now understand enums and their alternatives in TypeScript! Here's what to explore next:
Practice:
- Refactor magic strings in your code to enums
- Try using union types instead of enums
- Experiment with
as constobjects - Compare bundle sizes with const enums
Continue Learning:
- Interfaces and object types
- Type aliases and advanced types
- Generics for reusable types
- Utility types
Congratulations! You've mastered TypeScript enums and their alternatives. Use this knowledge to write more expressive, maintainable code. Remember: enums are a tool, not a requirement—choose the right approach for each situation.
Questions about enums? Debating between enums and union types? Share your use case in the comments and let's discuss the best approach!