SAE Logo

TypeScript Enums

Enums in TypeScript provide a way to define a set of named constants. They allow you to create a collection of related values that can be used as a type. Enums make your code more readable, maintainable, and less prone to errors by giving meaningful names to sets of numeric or string values.

Enums

©Net Ninja

Enum Basics

Enums create named constants representing a fixed set of values, providing both compile-time type safety and runtime values. They define a collection of related constants that can be used as types or values throughout your application.

Enums improve code readability by replacing magic numbers or strings with meaningful names.

enum Direction {
  North,
  East,
  South,
  West,
}

// Using the enum
let myDirection: Direction = Direction.North;
console.log(myDirection); // 0
console.log(Direction.East); // 1
console.log(Direction[0]); // "North"

In this example:

  • We define an enum called Direction with four values

  • By default, enum values are assigned incremental numeric values starting from 0

  • You can use the enum as a type annotation (let myDirection: Direction)

  • You can access the enum's numeric value or name using different syntaxes

Numeric Enums

Numeric enums assign incremental integer values starting from zero by default, though you can specify custom starting values. TypeScript automatically assigns subsequent values by incrementing from the previous value.

These enums support reverse mapping, allowing you to access both the name from the value and the value from the name.

enum Status {
  Pending, // 0
  Processing, // 1
  Completed, // 2
  Failed, // 3
}

let orderStatus: Status = Status.Processing;
console.log(orderStatus); // 1

Custom Numeric Values

You can assign specific numeric values to enum members, which is particularly useful for representing HTTP status codes, error codes, or other meaningful numeric constants. When you assign a value to one member, subsequent members continue auto-incrementing from that point unless explicitly assigned different 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("Request successful");
  } else if (status >= HttpStatus.BadRequest) {
    console.log("Request failed");
  }
}

handleResponse(HttpStatus.OK); // "Request successful"
handleResponse(HttpStatus.NotFound); // "Request failed"

When you assign a value to an enum member, subsequent members will be auto-incremented from that value:

enum Priority {
  Low = 5,
  Medium, // 6
  High, // 7
  Critical, // 8
}

Constant members are evaluated at compile time, while computed members are evaluated at runtime.

String Enums

String enums assign string values to each member, providing better debugging experience and more meaningful runtime values. Unlike numeric enums, string enums require explicit values for each member and don't support reverse mapping. They're ideal for representing states, categories, or any scenario where the string value has semantic meaning.

enum Direction {
  North = "NORTH",
  East = "EAST",
  South = "SOUTH",
  West = "WEST",
}

let myDirection: Direction = Direction.North;
console.log(myDirection); // "NORTH"

String enums have better readability and debugging experience compared to numeric enums because the values are meaningful when inspected at runtime. However, they don't support reverse mapping (you can't access Direction["NORTH"]).

Const Enums

Const enums are completely inlined during compilation, removing the enum object from the generated JavaScript and replacing enum references with their literal values. This provides performance benefits and smaller bundle sizes but eliminates runtime enum operations like iteration or reverse mapping.

const enum Direction {
  North,
  East,
  South,
  West,
}

let myDirection = Direction.North;

At compilation, const enums are completely removed and their values are inlined wherever they're used. In the JavaScript output, you'll see:

let myDirection = 0; // Direction.North

This results in more efficient code but prevents certain runtime operations like reverse mapping.

Enum Member Types

Individual enum members can serve as specific types, enabling precise type constraints where only certain enum values are acceptable. This creates more granular type safety than using the entire enum as a type, particularly useful for discriminated unions and state management patterns.

enum ShapeKind {
  Circle,
  Square,
  Triangle,
}

interface Circle {
  kind: ShapeKind.Circle;
  radius: number;
}

interface Square {
  kind: ShapeKind.Square;
  sideLength: number;
}

let circle: Circle = {
  kind: ShapeKind.Circle,
  radius: 10,
};

// This would cause a type error:
// let invalidCircle: Circle = {
//   kind: ShapeKind.Square,  // Error: Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'
//   radius: 10
// };

In this example, we're using specific enum members (ShapeKind.Circle and ShapeKind.Square) as types for the kind property, ensuring type safety.

Union Enums and Enum Member Types

Enums work seamlessly with union types, allowing you to create types that accept only specific subsets of enum values. This enables creating restricted APIs that only accept certain enum members while maintaining type safety and preventing invalid value usage.

enum Status {
  Active,
  Inactive,
  Pending,
}

// Use a subset of enum values as a type
type AvailableStatus = Status.Active | Status.Pending;

function processUser(userId: string, status: AvailableStatus) {
  // Process only users with Active or Pending status
}

// Valid
processUser("123", Status.Active);
processUser("456", Status.Pending);

// Invalid - will not compile
// processUser("789", Status.Inactive);

Enums at Runtime

Unlike other TypeScript type constructs, enums exist as real JavaScript objects at runtime, enabling dynamic operations like iteration and reflection. This runtime presence allows for more flexible programming patterns but also adds to bundle size, making const enums preferable when runtime features aren't needed.

enum Direction {
  North,
  East,
  South,
  West,
}

// You can pass the enum as a parameter
function printEnum(enumObject: any) {
  Object.keys(enumObject)
    .filter((key) => !isNaN(Number(key)))
    .forEach((key) => {
      console.log(`${key}: ${enumObject[key]}`);
    });
}

printEnum(Direction);
// 0: "North"
// 1: "East"
// 2: "South"
// 3: "West"

This runtime presence allows for more dynamic operations with enums, but also increases the size of your generated JavaScript.

Practical Examples

Example 1: User Roles

enum UserRole {
  Admin = "ADMIN",
  Editor = "EDITOR",
  Viewer = "VIEWER",
  Guest = "GUEST",
}

interface User {
  id: string;
  name: string;
  role: UserRole;
}

function checkAccess(user: User, requiredRole: UserRole): boolean {
  // Simple role hierarchy check
  switch (user.role) {
    case UserRole.Admin:
      return true; // Admin has access to everything
    case UserRole.Editor:
      return requiredRole !== UserRole.Admin;
    case UserRole.Viewer:
      return requiredRole === UserRole.Viewer || requiredRole === UserRole.Guest;
    case UserRole.Guest:
      return requiredRole === UserRole.Guest;
    default:
      return false;
  }
}

const user: User = {
  id: "user123",
  name: "Alice",
  role: UserRole.Editor,
};

console.log(checkAccess(user, UserRole.Viewer)); // true
console.log(checkAccess(user, UserRole.Admin)); // false

Example 2: State Machine for Order Processing

enum OrderState {
  Created,
  Processing,
  Shipped,
  Delivered,
  Canceled,
}

class Order {
  private state: OrderState;

  constructor(
    public id: string,
    public customerName: string
  ) {
    this.state = OrderState.Created;
  }

  getState(): OrderState {
    return this.state;
  }

  getStateAsString(): string {
    return OrderState[this.state];
  }

  processOrder(): boolean {
    if (this.state === OrderState.Created) {
      this.state = OrderState.Processing;
      console.log(`Order ${this.id} is now being processed`);
      return true;
    }
    console.log(`Cannot process order ${this.id} in ${this.getStateAsString()} state`);
    return false;
  }

  shipOrder(): boolean {
    if (this.state === OrderState.Processing) {
      this.state = OrderState.Shipped;
      console.log(`Order ${this.id} has been shipped`);
      return true;
    }
    console.log(`Cannot ship order ${this.id} in ${this.getStateAsString()} state`);
    return false;
  }

  deliverOrder(): boolean {
    if (this.state === OrderState.Shipped) {
      this.state = OrderState.Delivered;
      console.log(`Order ${this.id} has been delivered`);
      return true;
    }
    console.log(`Cannot deliver order ${this.id} in ${this.getStateAsString()} state`);
    return false;
  }

  cancelOrder(): boolean {
    if (this.state !== OrderState.Delivered && this.state !== OrderState.Canceled) {
      this.state = OrderState.Canceled;
      console.log(`Order ${this.id} has been canceled`);
      return true;
    }
    console.log(`Cannot cancel order ${this.id} in ${this.getStateAsString()} state`);
    return false;
  }
}

// Usage
const order = new Order("ORD123", "John Doe");
console.log(`New order state: ${order.getStateAsString()}`); // "Created"

order.processOrder(); // "Order ORD123 is now being processed"
order.shipOrder(); // "Order ORD123 has been shipped"
order.deliverOrder(); // "Order ORD123 has been delivered"

// Try to cancel a delivered order
order.cancelOrder(); // "Cannot cancel order ORD123 in Delivered state"

Best Practices for Using Enums

1. Use PascalCase for enum names and enum members

PascalCase follows TypeScript conventions for types and provides consistency with interfaces, classes, and type aliases. This naming pattern immediately identifies enum constructs and maintains visual coherence across your codebase. Consistent naming also aligns with community standards and popular style guides.

// Good
enum HttpStatus {
  OK = 200,
  NotFound = 404,
}

// Not recommended
enum httpStatus {
  ok = 200,
  notFound = 404,
}

2. Use string enums for better readability

String enums provide meaningful values during debugging and runtime inspection, making issues easier to diagnose. When examining logs, network requests, or runtime values, string enum members are self-documenting and immediately understandable. This improves developer experience and reduces debugging time compared to numeric enums that require lookup tables.

// Better - values are meaningful when debugging
enum Direction {
  North = "NORTH",
  East = "EAST",
  South = "SOUTH",
  West = "WEST",
}

// Less clear at runtime
enum Direction {
  North, // 0
  East, // 1
  South, // 2
  West, // 3
}

3. Use const enums for better performance

Const enums eliminate runtime overhead by inlining values during compilation, resulting in smaller bundle sizes and faster execution. When you don't need runtime enum operations like iteration or reverse mapping, const enums provide performance benefits without sacrificing type safety. This optimization is particularly valuable in performance-critical applications.

// More efficient - values are inlined
const enum Direction {
  North,
  East,
  South,
  West,
}

// Regular enum creates runtime object
enum Direction {
  North,
  East,
  South,
  West,
}

4. Consider alternatives to enums when appropriate

Union types of string literals often provide better type safety and smaller compiled output than traditional enums. For simple value sets, union types eliminate runtime objects entirely while maintaining excellent type checking. Const assertions with objects can provide namespace-like organization with superior type inference and tree-shaking capabilities.

// Union of string literals
type Direction = "North" | "East" | "South" | "West";

// For constant values that need a namespace
const HttpStatus = {
  OK: 200,
  Created: 201,
  BadRequest: 400,
  NotFound: 404,
} as const;
type HttpStatus = (typeof HttpStatus)[keyof typeof HttpStatus];

Additional Best Practices

Keep Enums Focused: Each enum should represent a single conceptual group of related values rather than mixing different categories of constants.

Document Complex Enums: Use JSDoc comments to explain enum purpose, valid use cases, and any business rules associated with specific values.

Avoid Computed Members: Stick to literal values for enum members to maintain predictability and enable better optimization by the TypeScript compiler.

Be Careful with Numeric Enums: When using numeric enums, ensure the numeric values are meaningful and won't cause confusion if exposed in APIs or debugging scenarios.

Consider Enum Stability: Once an enum is part of a public API, changing member values can break compatibility, so choose values carefully from the start.

Exercises

Exercise 1: Weekdays

Description

In this exercise, you'll create a simple enum to represent the days of the week and use it to build a function that determines whether a day is a weekday or weekend.

Instructions

  1. Create an enum called DaysOfWeek with seven values: MondayTuesdayWednesdayThursdayFridaySaturday, and Sunday.

  2. Write a function called isWeekend that:

    • Takes a parameter of type DaysOfWeek

    • Returns a boolean value: true if the day is a weekend (Saturday or Sunday), and false otherwise

  3. Test your function with different days of the week.

  4. Bonus: Create a function that returns the name of the next day given a current day.

Expected Output:

isWeekend(DaysOfWeek.Monday) should return false
isWeekend(DaysOfWeek.Saturday) should return true
nextDay(DaysOfWeek.Friday) should return DaysOfWeek.Saturday

Exercise 2: API Response Status

Description

In this exercise, you'll create a string enum to represent different API response statuses and build a function that simulates API responses with different statuses.

Instructions

  1. Create a string enum called ApiStatus with at least four values: SuccessErrorLoading, and Timeout. Assign appropriate string values to each.

  2. Write a function called simulateApiCall that:

    • Takes a parameter endpoint of type string

    • Returns a Promise that resolves with an ApiStatus value

    • Simulates an API call by waiting for 1-2 seconds (using setTimeout)

    • Randomly selects and returns one of the API statuses, with a higher probability for Success

  3. Write another function called handleApiResponse that:

    • Takes an ApiStatus parameter

    • Logs different messages based on the status

  4. Test your functions by making several simulated API calls and handling the responses.

Expected Output:

Calling API...
API returned status: SUCCESS
Data retrieved successfully

Calling API...
API returned status: ERROR
An error occurred while fetching data

Exercise 3: User Permissions System

Description

In this exercise, you'll create an enum-based permission system for a simple user management application. The system will define different permission levels and provide functions to check and manage user access.

Instructions

  1. Create an enum called UserPermission with the following values:

    • None

    • Read

    • Write

    • Delete

    • Admin

  2. Create a User interface with:

    • id: string

    • name: string

    • permission: UserPermission

  3. Create an array of sample users with different permission levels.

  4. Write the following functions:

    • canReadData(user: User): boolean - returns true if user has Read permission or higher

    • canWriteData(user: User): boolean - returns true if user has Write permission or higher

    • canDeleteData(user: User): boolean - returns true if user has Delete permission or higher

    • isAdmin(user: User): boolean - returns true if user has Admin permission

    • promoteUser(user: User): User - returns a new user object with permission increased by one level (up to Admin)

    • demoteUser(user: User): User - returns a new user object with permission decreased by one level (down to None)

  5. Test your functions with the sample users to verify they work correctly.

Expected Output:

User John (Read permission):
Can read: true
Can write: false
Can delete: false
Is admin: false

After promotion:
User John now has Write permission
Can read: true
Can write: true

Summary

TypeScript enums provide a powerful way to define sets of named constants that improve code readability and maintainability:

  • Numeric enums: The default, where members have numeric values

  • String enums: Where members have string values for better readability

  • Const enums: For better performance by inlining values at compile time

  • Computed members: Dynamic values determined at runtime

Key best practices:

  • Use PascalCase for enum names and members

  • Prefer string enums for better debugging

  • Use const enums for performance-critical code

  • Consider alternatives like union types when appropriate

  • Use powers of 2 for bit flags

Enums are a valuable tool in TypeScript's type system, helping you express intent more clearly and catch errors at compile time rather than runtime.

© 2025, SAE Academy