Discriminated Unions for success and error Responses
Replace a data-or-error object with a union tagged by success, then add a generic Outcome and an unwrap helper. After this, the compiler knows which property exists in each case.
Functions that can fail often return one object that has both data and error, with one of them empty. The type says either could be there, and the caller has to guess. A discriminated union gives the success case and the failure case separate shapes, with a single property to tell them apart. This page makes that change on a small function and shows what it buys. The lesson on discriminated unions in the course covers the wider idea, including three or more cases.
Step 1: the shape that makes callers guess
type Outcome = { data?: number; error?: string };
function divide(a: number, b: number): Outcome {
if (b === 0) {
return { error: "cannot divide by zero" };
}
return { data: a / b };
}
const outcome = divide(10, 2);
console.log(outcome.data.toFixed(1));
guess.ts(11,13): error TS18048: 'outcome.data' is possibly 'undefined'.
The compiler is right: data might be missing. But the fix everyone reaches for is outcome.data!, and that is a promise, not a check. The type also allows {} and { data: 1, error: "x" }, which no one means.
Step 2: one tag, two shapes
type Outcome<T> =
| { success: true; data: T }
| { success: false; error: string };
function divide(a: number, b: number): Outcome<number> {
if (b === 0) {
return { success: false, error: "cannot divide by zero" };
}
return { success: true, data: a / b };
}
for (const [a, b] of [[10, 4], [1, 0]]) {
const outcome = divide(a, b);
if (outcome.success) {
console.log(outcome.data.toFixed(2));
} else {
console.log(outcome.error);
}
}
2.50
cannot divide by zero
success is the tag. It is a boolean, which works as a tag because true and false are literal types. In the if, the compiler knows outcome is the success shape and has data. In the else, it has error. The generic <T> lets one Outcome type serve a function that returns a number, a user or a list.
Step 3: the compiler stops the mixed case
type Outcome<T> =
| { success: true; data: T }
| { success: false; error: string };
const both: Outcome<number> = { success: true, data: 1, error: "oops" };
const neither: Outcome<number> = { success: true };
mixed.ts(5,57): error TS2353: Object literal may only specify known properties, and 'error' does not exist in type '{ success: true; data: number; }'.
mixed.ts(6,7): error TS2322: Type '{ success: true; }' is not assignable to type 'Outcome<number>'.
Property 'data' is missing in type '{ success: true; }' but required in type '{ success: true; data: number; }'.
Both nonsense states are rejected. A success needs data and cannot carry an error.
Step 4: pull the data out safely
A caller who only wants the value can use a small helper that throws on failure.
type Outcome<T> =
| { success: true; data: T }
| { success: false; error: string };
function unwrap<T>(outcome: Outcome<T>): T {
if (outcome.success) {
return outcome.data;
}
throw new Error(outcome.error);
}
console.log(unwrap({ success: true, data: 42 }));
try {
unwrap({ success: false, error: "not found" });
} catch (error) {
console.log((error as Error).message);
}
42
not found
The function returns a plain T, never T | undefined. The failure is not hidden, it is moved to where you decided to throw.
⚠ Warning: A boolean tag only narrows when both cases are literal. If you type success as a plain boolean in a case, the compiler cannot tell the two shapes apart, and you are back to guessing.
Common mistakes
- Keeping both
data?anderror?optional and adding asuccessflag next to them. Then the flag is unrelated to the fields. - Checking
if (outcome.data). A value of0or an empty string is falsy and would be treated as a failure. - Using a different tag name in each type. Pick
success,okorstatusand keep it.
Related course: TypeScript Intermediate.
About
Replace a data-or-error object with a union tagged by success, then add a generic Outcome and an unwrap helper. After this, the compiler knows which property exists in each case.
- Responsible
- Christopher R
- Last Update
- 10/11/2026
- Reading Time
- 4 min
Reviews
Sign in to write a reviewComments (0)
Sign in to commentNo comments yet.