TypeScript

Enums in TypeScript

23 min Lesson 5 of 40

Understanding Enums

Enums (short for enumerations) are a TypeScript feature that allows you to define a set of named constants. They make your code more readable and self-documenting by giving meaningful names to sets of numeric or string values. Enums are particularly useful when you have a fixed set of related values that a variable can take.

Key Concept: Enums provide a way to organize collections of related values and give them friendly names. They help prevent magic numbers and strings in your code, making it more maintainable and less error-prone.

Numeric Enums

Basic Numeric Enums

By default, enums in TypeScript are numeric. The first value starts at 0 and each subsequent value increments by 1:

enum Direction {
    Up,      // 0
    Down,    // 1
    Left,    // 2
    Right    // 3
}

let playerDirection: Direction = Direction.Up;
console.log(playerDirection);  // 0

// Using in conditions
if (playerDirection === Direction.Up) {
    console.log("Moving up!");
}

// Enums are bidirectional - you can get the name from the value
let directionName = Direction[0];  // "Up"
console.log(directionName);

Custom Starting Values

You can set the starting value explicitly. Subsequent values will increment from there:

enum HttpStatus {
    OK = 200,
    Created = 201,
    Accepted = 202,
    BadRequest = 400,
    Unauthorized = 401,
    Forbidden = 403,
    NotFound = 404,
    InternalServerError = 500
}

function handleResponse(status: HttpStatus) {
    switch (status) {
        case HttpStatus.OK:
            console.log("Success!");
            break;
        case HttpStatus.NotFound:
            console.log("Resource not found");
            break;
        case HttpStatus.InternalServerError:
            console.log("Server error");
            break;
    }
}

handleResponse(HttpStatus.OK);  // "Success!"

Auto-Incrementing with Custom Values

You can set some values explicitly and let others auto-increment:

enum Level {
    Easy = 1,     // 1
    Medium,       // 2 (auto-increments)
    Hard,         // 3 (auto-increments)
    Expert = 10,  // 10
    Master        // 11 (auto-increments)
}

console.log(Level.Easy);    // 1
console.log(Level.Medium);  // 2
console.log(Level.Expert);  // 10
console.log(Level.Master);  // 11

Computed Enum Values

Enum values can be computed using expressions:

enum FileAccess {
    None = 0,
    Read = 1 << 0,     // 1
    Write = 1 << 1,    // 2
    ReadWrite = Read | Write,  // 3
    Execute = 1 << 2   // 4
}

// Checking permissions using bitwise operations
function hasPermission(access: FileAccess, permission: FileAccess): boolean {
    return (access & permission) === permission;
}

let myAccess = FileAccess.ReadWrite;
console.log(hasPermission(myAccess, FileAccess.Read));   // true
console.log(hasPermission(myAccess, FileAccess.Write));  // true
console.log(hasPermission(myAccess, FileAccess.Execute)); // false

String Enums

String enums allow you to assign string values to enum members. Unlike numeric enums, string enums don't auto-increment and each member must be explicitly initialized:

enum Color {
    Red = "RED",
    Green = "GREEN",
    Blue = "BLUE"
}

let favoriteColor: Color = Color.Red;
console.log(favoriteColor);  // "RED"

// String enums are more readable in runtime
function setTheme(color: Color) {
    console.log(`Setting theme to ${color}`);
}

setTheme(Color.Blue);  // "Setting theme to BLUE"

// String enums are NOT bidirectional
// Color["RED"]  // Error: Element implicitly has an 'any' type

Real-World String Enum Examples

// API endpoints
enum ApiRoute {
    Users = "/api/users",
    Posts = "/api/posts",
    Comments = "/api/comments",
    Auth = "/api/auth"
}

async function fetchData(route: ApiRoute) {
    const response = await fetch(route);
    return response.json();
}

fetchData(ApiRoute.Users);

// Event names
enum EventType {
    Click = "click",
    MouseOver = "mouseover",
    KeyDown = "keydown",
    Submit = "submit"
}

document.addEventListener(EventType.Click, (e) => {
    console.log("Clicked!");
});

// Database table names
enum TableName {
    Users = "users",
    Products = "products",
    Orders = "orders",
    OrderItems = "order_items"
}

function selectFrom(table: TableName, columns: string[]) {
    return `SELECT ${columns.join(", ")} FROM ${table}`;
}

console.log(selectFrom(TableName.Users, ["id", "name", "email"]));

When to Use String Enums: Use string enums when the actual value matters at runtime (like API routes, event names, or database operations). They provide better debugging experience because the values are meaningful strings rather than numbers.

Const Enums

Const enums are completely removed during compilation, and their values are inlined wherever they're used. This results in better performance and smaller bundle sizes:

const enum Direction {
    Up,
    Down,
    Left,
    Right
}

let direction = Direction.Up;

// After compilation, this becomes:
// let direction = 0;  // The enum is gone, only the value remains

// Benefits: No runtime code, better performance
// Drawback: Cannot use reverse mapping (Direction[0] doesn't work)

Regular Enum vs Const Enum

// Regular enum - generates runtime code
enum Status {
    Active,
    Inactive
}

// Compiles to:
// var Status;
// (function (Status) {
//     Status[Status["Active"] = 0] = "Active";
//     Status[Status["Inactive"] = 1] = "Inactive";
// })(Status || (Status = {}));

// Const enum - no runtime code
const enum Status {
    Active,
    Inactive
}

let status = Status.Active;
// Compiles to:
// let status = 0; // Just the value, no enum object

Const Enum Limitations: Const enums cannot be used with computed values, and you cannot get the enum object at runtime. Use regular enums if you need reflection or reverse mapping.

Heterogeneous Enums

TypeScript allows mixing string and numeric values in the same enum, though this is rarely recommended:

enum Mixed {
    No = 0,
    Yes = "YES"
}

console.log(Mixed.No);   // 0
console.log(Mixed.Yes);  // "YES"

// Reverse mapping only works for numeric values
console.log(Mixed[0]);   // "No"
// Mixed["YES"]  // Error

Best Practice: Avoid heterogeneous enums unless you have a very specific use case. Stick to either numeric or string enums for consistency and clarity.

Enum Member Types

Each enum member can be used as a type:

enum ShapeKind {
    Circle,
    Square
}

// Using enum member as a type
interface Circle {
    kind: ShapeKind.Circle;
    radius: number;
}

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

type Shape = Circle | Square;

function getArea(shape: Shape): number {
    switch (shape.kind) {
        case ShapeKind.Circle:
            return Math.PI * shape.radius ** 2;
        case ShapeKind.Square:
            return shape.sideLength ** 2;
    }
}

let circle: Circle = { kind: ShapeKind.Circle, radius: 10 };
console.log(getArea(circle));

Reverse Mapping

Numeric enums support reverse mapping, which means you can get the enum name from its value:

enum Priority {
    Low,      // 0
    Medium,   // 1
    High      // 2
}

// Forward mapping (name to value)
let value = Priority.High;  // 2

// Reverse mapping (value to name)
let name = Priority[2];     // "High"

// Iterating over enum names
for (let key in Priority) {
    if (isNaN(Number(key))) {
        console.log(key);  // "Low", "Medium", "High"
    }
}

// Iterating over enum values
for (let key in Priority) {
    if (!isNaN(Number(key))) {
        console.log(Priority[key]);  // "Low", "Medium", "High"
    }
}

Important: Reverse mapping only works with numeric enums. String enums and const enums do NOT support reverse mapping.

Enums at Runtime

Understanding how enums work at runtime helps you use them effectively:

enum Color {
    Red,
    Green,
    Blue
}

// The generated JavaScript object looks like:
// {
//   0: "Red",
//   1: "Green",
//   2: "Blue",
//   Red: 0,
//   Green: 1,
//   Blue: 2
// }

// You can check if a value is a valid enum member
function isValidColor(value: any): value is Color {
    return Object.values(Color).includes(value);
}

console.log(isValidColor(Color.Red));   // true
console.log(isValidColor(0));           // true
console.log(isValidColor(5));           // false

// Getting all enum keys
const colorKeys = Object.keys(Color).filter(k => isNaN(Number(k)));
console.log(colorKeys);  // ["Red", "Green", "Blue"]

// Getting all enum values (numbers)
const colorValues = Object.keys(Color).filter(k => !isNaN(Number(k))).map(Number);
console.log(colorValues);  // [0, 1, 2]

Enums vs Union Types

Sometimes you can use union types instead of enums. Here's when to use each:

// Using enum
enum Status {
    Active = "ACTIVE",
    Inactive = "INACTIVE",
    Pending = "PENDING"
}

function setStatus(status: Status) {
    console.log(status);
}

// Using union type
type Status = "ACTIVE" | "INACTIVE" | "PENDING";

function setStatus(status: Status) {
    console.log(status);
}

// Both approaches work, but have different characteristics

Enum Advantages:

  • Provides a namespace (Status.Active vs "ACTIVE")
  • Auto-completion works better
  • Can iterate over values
  • More self-documenting

Union Type Advantages:

  • No runtime code (when using string literals)
  • More flexible - can combine with other types
  • Better for library APIs (no enum dependency)
  • Simpler for small sets of values

Practical Enum Patterns

Pattern 1: Error Codes

enum ErrorCode {
    Success = 0,
    InvalidInput = 1001,
    DatabaseError = 2001,
    NetworkError = 3001,
    Unauthorized = 4001,
    NotFound = 4004
}

class Result<T> {
    constructor(
        public code: ErrorCode,
        public data?: T,
        public message?: string
    ) {}

    isSuccess(): boolean {
        return this.code === ErrorCode.Success;
    }
}

function fetchUser(id: number): Result<User> {
    if (id < 0) {
        return new Result(ErrorCode.InvalidInput, undefined, "Invalid user ID");
    }
    // ... fetch logic
    return new Result(ErrorCode.Success, userData);
}

Pattern 2: State Machine

enum OrderState {
    Created,
    PaymentPending,
    PaymentConfirmed,
    Processing,
    Shipped,
    Delivered,
    Cancelled
}

class Order {
    private state: OrderState = OrderState.Created;

    confirmPayment() {
        if (this.state === OrderState.PaymentPending) {
            this.state = OrderState.PaymentConfirmed;
        } else {
            throw new Error("Invalid state transition");
        }
    }

    ship() {
        if (this.state === OrderState.Processing) {
            this.state = OrderState.Shipped;
        } else {
            throw new Error("Cannot ship order in current state");
        }
    }

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

Pattern 3: Feature Flags

enum Feature {
    DarkMode = 1 << 0,        // 1
    Notifications = 1 << 1,   // 2
    AdvancedSearch = 1 << 2,  // 4
    AdminPanel = 1 << 3       // 8
}

class User {
    private features: number = 0;

    enableFeature(feature: Feature) {
        this.features |= feature;
    }

    disableFeature(feature: Feature) {
        this.features &= ~feature;
    }

    hasFeature(feature: Feature): boolean {
        return (this.features & feature) === feature;
    }
}

let user = new User();
user.enableFeature(Feature.DarkMode);
user.enableFeature(Feature.Notifications);

console.log(user.hasFeature(Feature.DarkMode));       // true
console.log(user.hasFeature(Feature.AdvancedSearch)); // false

Practice Exercise

Create a file called enums-practice.ts and complete these tasks:

  1. Create a DayOfWeek enum with all seven days. Write a function that determines if a day is a weekend.
  2. Create a string enum LogLevel with values: DEBUG, INFO, WARN, ERROR. Implement a simple logger class that only logs messages at or above a configured level.
  3. Create a Permission enum using bit flags for: Read, Write, Delete, Admin. Implement functions to grant, revoke, and check permissions.
  4. Create an HttpMethod enum and a generic request function that accepts the method and URL:
    enum HttpMethod {
        GET = "GET",
        POST = "POST",
        PUT = "PUT",
        DELETE = "DELETE"
    }
    
    async function request(method: HttpMethod, url: string, data?: any) {
        // Your implementation
    }
  5. Create a GameState enum (Menu, Playing, Paused, GameOver) and implement a simple game state manager that validates state transitions.
  6. Compare an enum solution with a union type solution for defining card suits. Implement a shuffle function for both approaches.

Bonus Challenge: Create a const enum for commonly used CSS color names and write a function that converts the enum to RGB values without any runtime enum code.

Common Mistakes to Avoid

  1. Using heterogeneous enums unnecessarily: Stick to numeric or string enums for consistency.
  2. Forgetting that numeric enums are bidirectional: Remember that MyEnum[0] returns the name.
  3. Using const enums when you need runtime reflection: Use regular enums if you need to iterate or use reverse mapping.
  4. Not initializing string enum members: Every member in a string enum must have a value.
  5. Comparing enums with strings/numbers incorrectly: Use the enum member (Color.Red) instead of its value (0).

Summary

  • Numeric enums start at 0 by default and auto-increment; support reverse mapping
  • String enums require explicit values; more readable at runtime; no reverse mapping
  • Const enums are inlined at compile time for better performance; no runtime object
  • Heterogeneous enums mix strings and numbers; avoid unless necessary
  • Enum members can be used as types for more precise type checking
  • Reverse mapping allows getting the name from numeric enum values
  • Enums generate runtime code (except const enums), providing namespace and iteration capabilities
  • Use enums for fixed sets of related values; consider union types for simpler cases
  • Bit flag patterns with enums enable efficient permission/feature systems

Next Steps: You've now mastered TypeScript's basic types, arrays, tuples, and enums. In the next lessons, we'll explore more advanced topics like interfaces, type aliases, generics, and advanced type features that make TypeScript truly powerful.