Narrowing uncertain values

Model states with a discriminated union

Give each variant a shared literal field so a switch can safely expose the fields belonging to that state.

A discriminated union models a value that is always in exactly one of several states. Each state is an object type, and every one of them shares a property with a different literal value:

type Result =
  | { status: 'success'; data: string }
  | { status: 'error'; message: string }

The status property is the discriminant. Every variant has it, and each gives it a different literal. That single field is enough for TypeScript to tell the variants apart.

Try reading result.data before checking status and the compiler stops you:

Property 'data' does not exist on type 'Result'.
  Property 'data' does not exist on type '{ status: "error"; message: string; }'.

The error is precise. data might not exist, because the value could be the error variant.

Narrow with a switch

Check the discriminant and TypeScript knows which other fields exist:

function printResult(result: Result) {
  switch (result.status) {
    case 'success':
      console.log(result.data)
      break
    case 'error':
      console.error(result.message)
      break
  }
}

Inside the 'success' case, result is the success variant, so result.data is a plain string. Inside 'error', only message exists. No casts, no optional chaining, no extra checks.

An if (result.status === 'error') works the same way. I reach for switch when there are more than two states, because each state gets its own visible block.

Why not optional fields

Compare this with one object that has optional data and message fields:

type WeakResult = {
  status: string
  data?: string
  message?: string
}

That weaker model allows impossible combinations, like a success without data, or an object carrying both fields at once. Every read becomes string | undefined, and you pay for the vagueness in every function that touches the value.

Discriminated unions work well for requests, reducers, messages, and UI states. Each state carries exactly the data it needs, and the compiler enforces the pairing.

Adding a state later

When you add a variant later, search for every switch on the discriminant. TypeScript flags the branches where the new variant leaks through, which turns a risky change into a guided checklist. Add a default branch that assigns result to a never variable and the compiler tells you the moment a case is missing.

Try this: add a { status: 'loading' } variant. Follow the compiler errors and update the code that assumed only success or error.

Lesson completed