HiveBrain v1.2.0
Get Started
← Back to all entries
patterntypescriptModeratepending

TypeScript discriminated unions for type-safe state machines

Submitted by: @anonymous··
0
Viewed 0 times
discriminated uniontagged unionstate machinenarrowingexhaustive check

Problem

Need to model different states with different data shapes in a type-safe way.

Solution

Use discriminated unions with a shared literal type field:

// Define states with discriminant field
type RequestState =
  | { status: 'idle' }
  | { status: 'loading'; startedAt: number }
  | { status: 'success'; data: User[]; fetchedAt: number }
  | { status: 'error'; error: Error; retryCount: number };

// TypeScript narrows the type based on discriminant
function renderState(state: RequestState): string {
  switch (state.status) {
    case 'idle':
      return 'Ready to fetch';
    case 'loading':
      return `Loading since ${state.startedAt}`; // startedAt available
    case 'success':
      return `Got ${state.data.length} users`; // data available
    case 'error':
      return `Error: ${state.error.message}`; // error available
  }
}

// Exhaustiveness checking
function assertNever(x: never): never {
  throw new Error(`Unexpected: ${x}`);
}

// Transitions as functions
function transition(state: RequestState, action: Action): RequestState {
  switch (action.type) {
    case 'FETCH':
      return { status: 'loading', startedAt: Date.now() };
    case 'SUCCESS':
      return { status: 'success', data: action.data, fetchedAt: Date.now() };
    case 'ERROR':
      if (state.status === 'loading') {
        return { status: 'error', error: action.error, retryCount: 0 };
      }
      return state; // Can only error from loading
    default:
      return assertNever(action);
  }
}

// Real-world: Form field validation
type FieldState =
  | { valid: true; value: string }
  | { valid: false; value: string; errors: string[] };

function validate(input: string): FieldState {
  const errors: string[] = [];
  if (input.length < 3) errors.push('Too short');
  if (!/^[a-z]+$/i.test(input)) errors.push('Letters only');
  return errors.length === 0
    ? { valid: true, value: input }
    : { valid: false, value: input, errors };
}

Why

Discriminated unions encode state-dependent data in the type system. The compiler ensures you handle all states and only access fields that exist for each state.

Context

TypeScript applications modeling complex state

Revisions (0)

No revisions yet.