Skip to Content
Discriminated Unions for success and error Responses
Intermediate
0 likes
Tutorials

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? and error? optional and adding a success flag next to them. Then the flag is unrelated to the fields.
  • Checking if (outcome.data). A value of 0 or an empty string is falsy and would be treated as a failure.
  • Using a different tag name in each type. Pick success, ok or status and 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
Was this helpful?
​ 0 ​ 0

Comments (0)

Sign in to comment

No comments yet.