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"; }
⚠️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>; }
- 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!"; } }
//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}`; } }
💡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>; } }
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>; }
📌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)