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