ThanhDev
  • Articles
  • Categories
  • Tags
  • About

ThanhDev

Frontend engineering notes on React, Next.js, TypeScript and software architecture.

  • Articles
  • Categories
  • Tags
  • About
  • GitHub
  • LinkedIn
  • Website
  • Email

© 2026 ThanhDev. Built with Next.js, Prisma and PostgreSQL.

  1. Home
  2. Articles
  3. TypeScript Discriminated Unions for Safer UI State
TypeScript

TypeScript Discriminated Unions for Safer UI State

Four booleans give you sixteen states, and twelve of them are nonsense. Model the four you actually have instead.

T

ThanhDev

Aug 23, 2026•2 min read•543 views

Most React state bugs I review are not logic errors. They are states the code allowed that the product never intended.

The shape that causes the bug

ts
type State = {
  isLoading: boolean;
  error: Error | null;
  data: User | null;
};

Three fields, eight combinations. What does { isLoading: true, error: someError, data: someUser } mean? Nobody knows, which is exactly why the spinner sometimes renders on top of an error message.

Make the invalid states unrepresentable

ts
type State =
  | { status: "idle" }
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: Error };

Four states, all meaningful. The compiler now enforces that data exists only when it can exist:

tsx
function UserView({ state }: { state: State }) {
  switch (state.status) {
    case "idle":
      return <Placeholder />;
    case "loading":
      return <Skeleton />;
    case "error":
      return <ErrorMessage error={state.error} />;
    case "success":
      return <Profile user={state.data} />;
  }
}

No state.data!, no optional chaining, no defensive checks.

Exhaustiveness is the real prize

Add a "refreshing" variant and every switch that forgot to handle it fails to compile — provided you ask for that check:

ts
function assertNever(value: never): never {
  throw new Error(`Unhandled variant: ${JSON.stringify(value)}`);
}
 
default:
  return assertNever(state);

That never parameter is the whole trick. If state can still be something, it will not be assignable to never, and you get an error at the call site.

Where else this pays off

  • API responses — { ok: true; data: T } | { ok: false; error: string } beats a nullable data with a nullable error.
  • Form submission — idle | submitting | submitted | failed.
  • Component props — a button that is either { href: string } or { onClick: () => void }, never both and never neither.
ts
type ButtonProps =
  | { href: string; onClick?: never }
  | { onClick: () => void; href?: never };

A boolean says "yes or no". A union says "here is the complete list of things that can be true". The second is almost always what you meant.

  • #TypeScript
  • #React
Share
PreviousDesigning a Prisma Schema That ScalesNextNext.js App Router: Server Components in Practice

Related articles

  • React

    Understanding React Rendering

    A practical explanation of how React rendering actually works under the hood, and why most re-render bugs are really state placement bugs.

    • #React
    • #Performance
    Sep 5, 2026•2 min read•49 views
  • Next.js

    Next.js App Router: Server Components in Practice

    Where the server/client boundary actually belongs, and how to stop accidentally shipping your whole application to the browser.

    • #Next.js
    • #React
    • #Server Components
    Aug 30, 2026•2 min read•882 views
  • Architecture

    Designing a Prisma Schema That Scales

    Relations, indexes and cascade rules are decisions you make once and live with for years. Here is how I approach them.

    • #Prisma
    • #PostgreSQL
    • #TypeScript
    Aug 15, 2026•2 min read•793 views