Unlocking the Power of TypeScript Enums and Unions

The Lure of Enums

Enums are essentially sets of named constants that can take either numeric or string forms. At first, I used enums everywhere, from Redux actions to finite state machines, to avoid pesky string typo errors.

The Limitations of Enums

Take, for instance, an AuthenticationStates enum that models an authentication workflow:

enum AuthenticationStates {
  UNAUTHENTICATED,
  AUTHENTICATING,
  AUTHENTICATED,
  ERROR
}

While enums ensure that a user can’t be in multiple contradictory states at once, they fall short when it comes to handling additional data, such as error objects.

Enter Discriminated Unions

That’s when I discovered discriminated unions, also known as algebraic data types. These powerful constructs allow us to create unions that can be composed of many types, not just primitive ones.

type AuthenticationState =
  | { type: 'UNAUTHENTICATED' }
  | { type: 'AUTHENTICATING' }
  | { type: 'AUTHENTICATED', authToken: string }
  | { type: 'ERROR', error: Error }

By introducing a discriminator field (type in this example), we can create a union that only requires the same kind field as each element in the union.

Type Narrowing and Compiler Enforcement

The true magic happens when TypeScript type narrows on a discriminator of a union. This means that only the appropriate data is available on each type, ensuring that we can’t access an authToken if we’re not in the AUTHENTICATED state.

function handleAuthState(state: AuthenticationState) {
  switch (state.type) {
    case 'AUTHENTICATED':
      console.log(state.authToken); // okay
      break;
    default:
      console.log(state.authToken); // error: Property 'authToken' does not exist on type '{ type: "UNAUTHENTICATED" } | { type: "AUTHENTICATING" } | { type: "ERROR"; error: Error; }'.
  }
}

The compiler enforces this correctness, making it impossible to access incorrect data.

Booleans Don’t Cut It

Attempts to model state with Booleans inevitably lead to a mess of tangled logic and contradicting variables. Algebraic data types, on the other hand, provide a elegant solution to this problem.

Unions: The Ultimate Executable Documentation

Discriminated unions serve as beautiful, executable documentation that keeps developers on the right track. They’ve been around in functional languages like Haskell, and it’s exciting to see TypeScript bring them to the frontend development community.

Leave a Reply