0
0
Typescriptprogramming~5 mins

Discriminated union state machines in Typescript

Choose your learning style9 modes available
Introduction

Discriminated union state machines help you manage different states clearly and safely in your program. They make it easy to know what state you are in and what actions you can do next.

When you want to track different steps in a process, like a login flow.
When you need to handle different modes of a feature, like a media player (playing, paused, stopped).
When you want to avoid mistakes by making sure only valid states and transitions happen.
When you want your code to be easier to read and maintain by clearly defining states.
When you want TypeScript to help catch errors about states before running the program.
Syntax
Typescript
type State =
  | { type: 'idle' }
  | { type: 'loading' }
  | { type: 'success'; data: string }
  | { type: 'error'; message: string };

function handleState(state: State) {
  switch (state.type) {
    case 'idle':
      // handle idle
      break;
    case 'loading':
      // handle loading
      break;
    case 'success':
      // handle success with state.data
      break;
    case 'error':
      // handle error with state.message
      break;
  }
}

The type property is the discriminant that tells TypeScript which state it is.

Each state can have its own extra properties to hold data related to that state.

Examples
This defines a traffic light state machine with three states.
Typescript
type LightState =
  | { type: 'red' }
  | { type: 'yellow' }
  | { type: 'green' };
This example shows a login state machine with a user name in the loggedIn state.
Typescript
type AuthState =
  | { type: 'loggedOut' }
  | { type: 'loggingIn' }
  | { type: 'loggedIn'; user: string };

function printState(state: AuthState) {
  switch (state.type) {
    case 'loggedOut':
      console.log('User is logged out');
      break;
    case 'loggingIn':
      console.log('User is logging in');
      break;
    case 'loggedIn':
      console.log(`Welcome, ${state.user}`);
      break;
  }
}
Sample Program

This program models a door that can be closed, opening, open, or closing. It moves through states in order and prints each state.

Typescript
type DoorState =
  | { type: 'closed' }
  | { type: 'opening' }
  | { type: 'open' }
  | { type: 'closing' };

function nextState(state: DoorState): DoorState {
  switch (state.type) {
    case 'closed':
      return { type: 'opening' };
    case 'opening':
      return { type: 'open' };
    case 'open':
      return { type: 'closing' };
    case 'closing':
      return { type: 'closed' };
  }
}

let state: DoorState = { type: 'closed' };
console.log(`Initial state: ${state.type}`);

state = nextState(state);
console.log(`Next state: ${state.type}`);

state = nextState(state);
console.log(`Next state: ${state.type}`);

state = nextState(state);
console.log(`Next state: ${state.type}`);

state = nextState(state);
console.log(`Next state: ${state.type}`);
OutputSuccess
Important Notes

Always use a unique type string for each state to help TypeScript distinguish them.

Using a switch statement on the discriminant helps TypeScript know which properties are available.

This pattern helps prevent bugs by making invalid states or transitions impossible to represent.

Summary

Discriminated unions let you define clear, safe states with a common type property.

Use them to build simple state machines that are easy to understand and maintain.

TypeScript helps catch mistakes by knowing exactly which state you are working with.