The most powerful patterns in TypeScript, Discriminated Unions

===

Most useful in cases where you have known result with list of known attribute value.

TypeScript will narrow the type based on the discriminator, a common property to discriminate between union members. , making your code much safer.

type State = 
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: string }
  | { status: 'error'; error: Error }

function handleState(state: State) {
  switch (state.status) {
    case 'idle':
      // TypeScript knows state is { status: 'idle' }
      break
    case 'loading':
      // TypeScript knows state is { status: 'loading' }
      break
    case 'success':
      // TypeScript knows state has 'data' property
      console.log(state.data.toUpperCase())
      break
    case 'error':
      // TypeScript knows state has 'error' property
      console.error(state.error.message)
      break
  }
}

The most powerful patterns in TypeScript. Discriminated Unions allow you to create a type-safe state machine where the compiler ensures you only access data that actually exists in a given state.

Here is a breakdown of why this pattern is the gold standard for state management.

1. The Core Components

To create a Discriminated Union, you need three things:

  1. The Members: Individual object types representing different states.
  2. The Discriminant: A common property with a literal type (like 'idle', 'loading') that exists in every member.
  3. The Union: A type that combines them using the | operator.

Example Anatomy

type State = 
  | { status: 'idle' } 
  | { status: 'loading' } 
  | { status: 'success'; data: string } // 'data' only exists here
  | { status: 'error'; error: Error };   // 'error' only exists here

2. Why It Beats “Optional Property” Hell

Without unions, developers often use one giant object with optional properties. This is dangerous because it allows for “impossible states.”

The “Bad” Way (Optional Properties) The “Good” Way (Discriminated Unions)
data?: string; error?: Error; Data and Error are tied to specific statuses.
You could accidentally have both data and error at the same time. The type system makes it impossible to have data while in an error state.
Requires constant null checks or “non-null assertions” (!). TypeScript narrows the type automatically.

3. Type Narrowing in Action

As you showed in your post’s switch statement, once you check the status property, TypeScript “narrows” the object to that specific member of the union.

function handleState(state: State) {
  if (state.status === 'success') {
    // Inside this block, TypeScript knows 'data' exists.
    // You don't need to check if state.data is undefined.
    console.log(state.data); 
  }
}

4. Exhaustiveness Checking

One of the best “pro tips” for state management is ensuring you’ve handled every possible state. You can use the never type to catch unhandled cases at compile time:

function handleState(state: State) {
  switch (state.status) {
    case 'idle': return 'Waiting...';
    case 'loading': return 'Loading...';
    case 'success': return state.data;
    case 'error': return state.error.message;
    default:
      // If you add a new state like 'processing' later, 
      // TypeScript will throw an error here because 'processing'
      // cannot be assigned to 'never'.
      const _exhaustiveCheck: never = state;
      return _exhaustiveCheck;
  }
}

Summary of Benefits

  • Safety: Prevents accessing data when the app is still loading.
  • Clarity: The code serves as documentation for what data is available when.
  • Maintainability: Adding a new state (e.g., reconnecting) triggers compiler errors in every function that hasn’t accounted for it yet.
Total
0
Shares
Leave a Reply

Your email address will not be published. Required fields are marked *

Previous Post

Iran threatens ‘Stargate’ AI data centers

Next Post

Google quietly launched an AI dictation app that works offline

Related Posts