TypeScript Enums: A Comprehensive Tutorial

Enums in TypeScript are a way of giving more friendly names to sets of numeric or string values. They allow developers to define a collection of related values that can be numeric or string-based, making your code more readable and maintainable.

Enums help by providing a mechanism to associate meaningful names with values, especially when you need to represent a fixed set of constants.

This tutorial will cover the basics of enums, including how to define and use them, how numeric and string enums work, reverse mapping, and some advanced enum features in TypeScript.

1. What is an Enum?

In TypeScript, an enum is a way of defining a set of named constants. It can be either numeric or string. Enums make code easier to read and maintain by giving meaningful names to constant values.

2. Defining Numeric Enums

The most basic form of an enum is a numeric enum, where the values are automatically assigned numbers (starting from 0 by default).

Example 1: Basic Numeric Enum

enum Direction {
  Up,
  Down,
  Left,
  Right
}

console.log(Direction.Up);    // Output: 0
console.log(Direction.Down);  // Output: 1
console.log(Direction.Left);  // Output: 2
console.log(Direction.Right); // Output: 3

Explanation:

The enum Direction has four members (Up, Down, Left, Right). TypeScript automatically assigns 0 to Up, 1 to Down, and so on.
You can access the enum values using Direction.Up, which returns 0.

3. Customizing Enum Values

You can customize the values assigned to enum members. If you assign a specific value to one member, the subsequent members will continue incrementing from that value.

Example 2: Custom Enum Values

enum Direction {
  Up = 1,
  Down,
  Left = 5,
  Right
}

console.log(Direction.Up);    // Output: 1
console.log(Direction.Down);  // Output: 2 (auto-incremented)
console.log(Direction.Left);  // Output: 5
console.log(Direction.Right); // Output: 6 (auto-incremented)

Explanation:

Up is explicitly assigned the value 1. The next member (Down) is auto-incremented to 2.
Left is explicitly assigned 5, and Right is auto-incremented to 6.

4. String Enums

In string enums, each member must be explicitly assigned a string value. String enums provide better readability, especially when the values are meaningful strings instead of numbers.

Example 3: String Enums

enum Status {
  Success = 'SUCCESS',
  Error = 'ERROR',
  Loading = 'LOADING'
}

console.log(Status.Success);  // Output: 'SUCCESS'
console.log(Status.Error);    // Output: 'ERROR'
console.log(Status.Loading);  // Output: 'LOADING'

Explanation:

The Status enum assigns meaningful string values to each member.
String enums require you to manually assign a string value to each member.

5. Heterogeneous Enums (Mixing String and Numeric Values)

You can define enums that mix both numeric and string values, but this practice is generally discouraged because it can lead to confusion.

Example 4: Heterogeneous Enum

enum MixedEnum {
  No = 0,
  Yes = 'YES'
}

console.log(MixedEnum.No);  // Output: 0
console.log(MixedEnum.Yes); // Output: 'YES'

Explanation:

The MixedEnum has both numeric (0) and string (‘YES') values. While this is allowed, it should be used sparingly as it can make code harder to understand.

6. Reverse Mapping in Numeric Enums

TypeScript provides a feature called reverse mapping for numeric enums, which allows you to retrieve the name of an enum member based on its value.

Example 5: Reverse Mapping in Numeric Enums

enum Colors {
  Red,
  Green,
  Blue
}

console.log(Colors.Red);    // Output: 0
console.log(Colors[0]);     // Output: 'Red'
console.log(Colors[1]);     // Output: 'Green'
console.log(Colors[2]);     // Output: 'Blue'

Explanation:

You can access both the value and the name of a numeric enum. For example, Colors.Red returns 0, and Colors[0] returns ‘Red'.
Reverse mapping only works with numeric enums, not string enums.

7. Computed and Constant Members

Enum members can either be constant (values known at compile-time) or computed (values calculated at runtime). Constant members are simpler and evaluated when the enum is compiled, while computed members are calculated dynamically.

Example 6: Computed and Constant Members

enum Role {
  Admin = 1,
  User = 2,
  Guest = 'GUEST'.length  // Computed member
}

console.log(Role.Admin);  // Output: 1
console.log(Role.User);   // Output: 2
console.log(Role.Guest);  // Output: 5 ('GUEST'.length is 5)

Explanation:

Admin and User are constant members.
Guest is a computed member, as its value depends on the result of ‘GUEST'.length, which is evaluated at runtime.

8. Enums with Type Annotations

Enums can be used in function parameters and return types to ensure type safety.

Example 7: Using Enums in Function Parameters

enum Direction {
  Up,
  Down,
  Left,
  Right
}

function move(direction: Direction): void {
  switch (direction) {
    case Direction.Up:
      console.log('Moving up');
      break;
    case Direction.Down:
      console.log('Moving down');
      break;
    case Direction.Left:
      console.log('Moving left');
      break;
    case Direction.Right:
      console.log('Moving right');
      break;
  }
}

move(Direction.Up);    // Output: Moving up
move(Direction.Right); // Output: Moving right

Explanation:

The function move() accepts a parameter of type Direction, ensuring that only valid enum values can be passed.
The switch statement handles each enum value, and TypeScript will give a compile-time error if an invalid value is passed to the function.

9. Using const enum

In some scenarios, you can define a const enum to optimize the performance. const enum members are inlined by the compiler at compile-time, removing the need for reverse mapping.

Example 8: Using const enum

const enum Days {
  Monday,
  Tuesday,
  Wednesday,
  Thursday,
  Friday
}

let workday: Days = Days.Wednesday;
console.log(workday);  // Output: 2 (No reverse mapping available)

Explanation:

const enum values are replaced with their constant values at compile-time, resulting in better performance and smaller compiled code.
However, reverse mapping (e.g., Days[2]) is not available for const enum.

10. Enum as a Type

Enums can also be used as types, allowing you to restrict the possible values of a variable to those defined in the enum.

Example 9: Enum as a Type

enum TrafficLight {
  Red,
  Yellow,
  Green
}

let currentLight: TrafficLight = TrafficLight.Red;

console.log(currentLight);  // Output: 0 (the value of TrafficLight.Red)

// This will cause an error because 'Blue' is not part of the TrafficLight enum
// currentLight = TrafficLight.Blue;  // Error: Property 'Blue' does not exist on type 'typeof TrafficLight'.

Explanation:

The currentLight variable can only hold values from the TrafficLight enum, ensuring type safety.
Attempting to assign a value that is not part of the enum will result in a compile-time error.

11. Enums with Union Types

Enums can be combined with union types to create more complex type definitions. This can be useful when you want to allow both enum members and specific values.

Example 10: Enum with Union Types

enum Size {
  Small,
  Medium,
  Large
}

type ShirtSize = Size | 'Extra Large';

let size: ShirtSize = Size.Small;
console.log(size);  // Output: 0

size = 'Extra Large';
console.log(size);  // Output: 'Extra Large'

// This will cause an error because 'Tiny' is not part of the ShirtSize type
// size = 'Tiny';  // Error: Type '"Tiny"' is not assignable to type 'ShirtSize'.

Explanation:

The ShirtSize type can hold either an enum value from Size or the string ‘Extra Large'.
Attempting to assign a value not included in the union type (like ‘Tiny') will result in an error.

Conclusion

Enums in TypeScript provide a convenient way to define sets of related constants with either numeric or string values. They help make your code more readable, maintainable, and type-safe by restricting values to specific names.

Understanding how to use enums effectively will allow you to create cleaner and more robust TypeScript applications.

Key Topics Covered:

Numeric Enums: Automatically or manually assign numbers to enum members.
String Enums: Use meaningful string values for enum members.
Custom Enum Values: Customize enum values to meet your specific needs.
Reverse Mapping: Retrieve the name of a numeric enum member from its value.
Computed and Constant Members: Use both compile-time and runtime values in enums.
Enums in Functions: Use enums in function parameters and return types.
const enum: Optimize performance by inlining enum values.
Enums as Types: Use enums to restrict values in variables or function arguments.
Union Types with Enums: Combine enums with union types for more flexible type definitions.

By mastering TypeScript enums, you can make your code more robust and easier to understand, while taking advantage of type safety and clean error handling.

Related posts

TypeScript null & undefined: A Comprehensive Tutorial with Code Examples

TypeScript Casting: A Comprehensive Tutorial with Code Examples

TypeScript Functions: A Comprehensive Tutorial with Code Examples