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.

Enum Basics

Here's a basic enum definition in TypeScript:

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

By default, enums in TypeScript are numeric. The first value is assigned 0, and subsequent values are incremented by 1:

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 custom numeric values to enum members:

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

TypeScript also supports string enums, where each member has a string value:

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

The const keyword can be used with enums to improve performance:

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 also serve as types. Let's look at an example:

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 can be combined with union types to create powerful type constraints:

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

Enums exist at runtime as real objects. This is different from TypeScript's other type constructs (like interfaces), which are erased during compilation:

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"

This example demonstrates using enums to track the state of an order through its lifecycle.

Best Practices for Using Enums

1. Use PascalCase for enum names and enum members

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

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

2. Use string enums for better readability

// 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 when possible

// 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

TypeScript offers other ways to represent a fixed set of values:

// 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];

These alternatives can sometimes offer better type safety or smaller compiled code.

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: Monday, Tuesday, Wednesday, Thursday, Friday, Saturday, 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: Success, Error, Loading, 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.