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. Use one shared literal property to identify every valid variant:

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

The status property is the discriminant. Every variant has it, and each variant gives it a different literal value. 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

After checking the discriminant, 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.

Why not optional fields

This is safer than one object with optional data and message fields:

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

That weaker model allows impossible combinations, such as 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.

One practical tip: 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.

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

Lesson completed

Take this course offline

Get every free book, course edition, and software download.

Get the download library →