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)