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)