DEV Community

Cover image for Discriminated Unions in TypeScript: How They Differ from Plain Type Unions
Arka Chakraborty
Arka Chakraborty

Posted on

Discriminated Unions in TypeScript: How They Differ from Plain Type Unions

Type Union vs Discriminated Unions


All unions are not equal, some are smart enough to know what fields exist in each branch.


🧠Context

React components often handle multiple states: forms (loading, success, error), modals (open, closed), or API responses (data vs error).
A common confusion is the difference between plain type unions and discriminated unions. Understanding this distinction helps you write safer, more maintainable React components.


📌Plain Type Unions

A plain union is a type that can hold multiple literal values or types, but TypeScript does not associate additional fields with each value.

type Status = "loading" | "success" | "error";

function render(status: Status) {
  if (status === "success") {
    // ❌ Can't attach extra info like "data"
    return "Form submitted!";
  }
  return "Not submitted";
}

Enter fullscreen mode Exit fullscreen mode

⚠️Limitations of plain unions:

  • No structured data per state
  • No automatic type narrowing for properties
  • Good only for simple flags

📌Discriminated Unions

  • A discriminated union is a union of objects where each object has:
  • A discriminant property (usually a literal string like "status" or "type")
  • Optional additional fields unique to that branch

📝How TypeScript narrows types

type FormState =
  | { status: "loading" }
  | { status: "success"; data: string }
  | { status: "error"; message: string };

function renderForm(state: FormState) {
  if (state.status === "loading") return <p>Loading...</p>;
  if (state.status === "success") return <p>Submitted: {state.data}</p>;
  if (state.status === "error") return <p>Error: {state.message}</p>;
}

Enter fullscreen mode Exit fullscreen mode
  • TS automatically narrows the type of state based on the discriminant (status)
  • Each branch has access only to its relevant fields (data, message)

📌With vs Without Discriminated Unions

//Without discriminated unions

type FormState = "loading" | "success" | "error";

function renderForm(state: FormState) {
  if (state === "success") {
    // ❌ Can't attach extra info like data
    return "Form submitted!";
  }
}

Enter fullscreen mode Exit fullscreen mode
//With discriminated unions

type FormState =
  | { status: "loading" }
  | { status: "success"; data: string }
  | { status: "error"; message: string };

function renderForm(state: FormState) {
  if (state.status === "success") {
    // ✅ TS knows 'data' exists
    return `Form submitted with: ${state.data}`;
  }
}

Enter fullscreen mode Exit fullscreen mode

💡Real-Life React Use Cases

Form State Example

type FormState =
  | { status: "idle" }
  | { status: "submitting" }
  | { status: "success"; result: string }
  | { status: "error"; message: string };

function Form({ state }: { state: FormState }) {
  switch (state.status) {
    case "idle": return <button>Submit</button>;
    case "submitting": return <p>Submitting...</p>;
    case "success": return <p>{state.result}</p>;
    case "error": return <p>{state.message}</p>;
  }
}

Enter fullscreen mode Exit fullscreen mode

Modal Example

type ModalState =
  | { open: true; content: string }
  | { open: false };

function Modal({ state }: { state: ModalState }) {
  if (!state.open) return null;
  return <div>{state.content}</div>;
}

Enter fullscreen mode Exit fullscreen mode

📌Key Takeaways

  • Plain unions are fine for flags, but they cannot carry per-case data.
  • Discriminated unions let TypeScript know exactly which fields exist per case, making components safer.
  • For React apps, always prefer discriminated unions for component states, API responses, and mutually exclusive props.

Top comments (0)