All posts
·#TypeScript#Patterns

Discriminated Unions Are the Best Part of TypeScript

Replacing optional flags with a discriminant property cleans up component props and unlocks exhaustive checks.

Discriminated unions let TypeScript narrow types based on a literal tag. They turn fuzzy "any of these flags might be set" props into precise, mutually exclusive shapes.

The problem

A typical Button component starts simple, then grows props for every variant: href, onClick, isLoading, disabled. Soon callers can pass nonsense like href and onClick at the same time.

The fix

type ButtonProps =
  | { kind: "link"; href: string; children: React.ReactNode }
  | { kind: "button"; onClick: () => void; children: React.ReactNode };

function Button(props: ButtonProps) {
  if (props.kind === "link") {
    return <a href={props.href}>{props.children}</a>;
  }
  return <button onClick={props.onClick}>{props.children}</button>;
}

Now the wrong props are unreachable. Pair with an assertNever helper in switches for exhaustiveness.

Rule of thumb: if two props should never appear together, they belong to different variants of a union.