A discriminated union is a TypeScript pattern for creating a type that can be one of several variants, each identified by a specific literal property (the “discriminator”).
It’s very useful for conditional rendering in React because it forces exhaustive checks — TypeScript will warn you if you forget a case.
type LoadingState = { status: 'loading' };
type SuccessState = { status: 'success'; data: string[] };
type ErrorState = { status: 'error'; message: string };
type FetchState = LoadingState | SuccessState | ErrorState;Here, the status property is the discriminator.
type LoadingState = { status: 'loading' };
type SuccessState = { status: 'success'; data: string[] };
type ErrorState = { status: 'error'; message: string };
type FetchState = LoadingState | SuccessState | ErrorState;
const DataComponent: React.FC<{ state: FetchState }> = ({ state }) => {
switch (state.status) {
case 'loading':
return <p>Loading...</p>;
case 'success':
return (
<ul>
{state.data.map((item, i) => (
<li key={i}>{item}</li>
))}
</ul>
);
case 'error':
return <p>Error: {state.message}</p>;
default:
// This will trigger a TypeScript error if a case is missing
const _exhaustiveCheck: never = state;
return _exhaustiveCheck;
}
};- Type Safety — No accidentally accessing data when in "loading" state.
- Exhaustiveness Checking — TypeScript errors if you miss a case.
- Clear State Management — Each variant is explicit.
- API states:
"idle" | "loading" | "success" | "error" - Component modes:
"view" | "edit" | "create" - Form states:
"valid" | "invalid" | "submitting"
type ApiState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; message: string };
function useApi<T>(url: string): ApiState<T> {
const [state, setState] = React.useState<ApiState<T>>({ status: 'idle' });
React.useEffect(() => {
setState({ status: 'loading' });
fetch(url)
.then((res) => res.json())
.then((data) => setState({ status: 'success', data }))
.catch((err) => setState({ status: 'error', message: err.message }));
}, [url]);
return state;
}
// And in a component:
const state = useApi<User[]>('/api/users');
switch (state.status) {
case 'idle':
return <p>Waiting...</p>;
case 'loading':
return <p>Loading...</p>;
case 'success':
return <UserList users={state.data} />;
case 'error':
return <p>{state.message}</p>;
}- Discriminated unions pair a literal type with unique properties.
- Helps in safe conditional rendering.
- Forces you to handle all possible states at compile time.
- Common with API data fetching patterns.