0Pricing
React Academy · Lesson

Discriminated Unions for Component Variants

Model variant props with discriminated unions so TypeScript enforces valid prop combinations.

Discriminated Unions for Component Variants is a free React Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the React Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.

The Problem with Variant Props

A component with optional props for different modes (e.g., a button that's either a link or a button) can have invalid prop combinations. TypeScript can't catch them without discriminated unions.

What Is a Discriminated Union?

A discriminated union is a union of types that share a common literal type field (the discriminant). TypeScript narrows the type based on that field's value.

type Shape =
  | { kind: 'circle'; radius: number }
  | { kind: 'rectangle'; width: number; height: number };

function area(shape: Shape): number {
  if (shape.kind === 'circle') return Math.PI * shape.radius ** 2;
  return shape.width * shape.height; // TS knows width/height exist here
}

Button vs Link Variant

Model a polymorphic component with a discriminated union on a as or variant field to enforce the correct props for each case.

type ButtonProps =
  | { as: 'button'; onClick: () => void; disabled?: boolean; children: React.ReactNode }
  | { as: 'a'; href: string; target?: string; children: React.ReactNode };

function ActionButton(props: ButtonProps) {
  if (props.as === 'button') {
    return <button onClick={props.onClick} disabled={props.disabled}>{props.children}</button>;
  }
  return <a href={props.href} target={props.target}>{props.children}</a>;
}

Alert Component Variants

Model an Alert with different required data for each severity type.

type AlertProps =
  | { type: 'success'; message: string }
  | { type: 'error'; message: string; onRetry: () => void }
  | { type: 'warning'; message: string; details?: string };

function Alert(props: AlertProps) {
  if (props.type === 'error') {
    return (
      <div className="alert error">
        <p>{props.message}</p>
        <button onClick={props.onRetry}>Retry</button>
      </div>
    );
  }
  return <div className={`alert ${props.type}`}>{props.message}</div>;
}

Narrowing in Event Handlers

Discriminated unions work with event-driven data too — great for state machines or action-based state.

type LoadingState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function DataView<T>({ state }: { state: LoadingState<T> }) {
  if (state.status === 'loading') return <Spinner />;
  if (state.status === 'error') return <p>{state.error.message}</p>;
  if (state.status === 'success') return <pre>{JSON.stringify(state.data)}</pre>;
  return null;
}

Exhaustive Checks with never

Add a never check in the default branch to get a TypeScript error if a new union member is added but not handled.

function assertNever(x: never): never {
  throw new Error('Unhandled case: ' + x);
}

function renderIcon(type: AlertProps['type']) {
  switch (type) {
    case 'success': return <CheckIcon />;
    case 'error': return <XIcon />;
    case 'warning': return <WarnIcon />;
    default: return assertNever(type); // TS error if a case is missing
  }
}

Discriminated Unions for API Responses

Model API response shapes as discriminated unions so callers handle success and error paths without casting.

type ApiResult<T> =
  | { ok: true; data: T }
  | { ok: false; error: string; code: number };

async function fetchUser(id: string): Promise<ApiResult<User>> {
  const res = await fetch(`/api/users/${id}`);
  if (!res.ok) return { ok: false, error: 'Not found', code: res.status };
  return { ok: true, data: await res.json() };
}

Type Guards for Narrowing

Use custom type guards to narrow unions in more complex scenarios where the discriminant isn't a simple equality check.

function isSuccess<T>(result: ApiResult<T>): result is { ok: true; data: T } {
  return result.ok === true;
}

const result = await fetchUser('1');
if (isSuccess(result)) {
  console.log(result.data.name); // TS knows data exists
}

Avoiding Optional Prop Soup

Without discriminated unions, components accumulate optional props that are only valid in certain combinations — confusing and un-typed. Discriminated unions eliminate impossible states.

// Bad: optional prop soup — invalid combos allowed:
interface BadProps {
  href?: string;
  onClick?: () => void;
  disabled?: boolean;
}

// Good: only valid combos via discriminated union:
type GoodProps =
  | { as: 'a'; href: string }
  | { as: 'button'; onClick: () => void; disabled?: boolean };

Composing Unions

Use & (intersection) to add shared props to all members of a union.

type BaseProps = { className?: string; children: React.ReactNode };

type ButtonVariant =
  | (BaseProps & { variant: 'primary'; onClick: () => void })
  | (BaseProps & { variant: 'link'; href: string });

Runtime Discrimination

React uses the discriminant at runtime to render the right UI. TypeScript uses it at compile time to enforce correct prop usage. Both layers are protected.

Quick Check

What is the discriminant in a discriminated union?

Recap

Discriminated unions model components with mutually exclusive prop sets by sharing a literal-typed discriminant field. Use never in the default branch for exhaustive checks. They eliminate invalid prop combinations that optional props allow, making components self-documenting and type-safe.

Frequently asked questions

Is the “Discriminated Unions for Component Variants” lesson free?

Yes — the full text of “Discriminated Unions for Component Variants” is free to read here on the web, and the React Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the React Academy course, upgrade to CoddyKit PRO.

What will I learn in “Discriminated Unions for Component Variants”?

Model variant props with discriminated unions so TypeScript enforces valid prop combinations. You practise React Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.

Do I need any experience to start React Academy?

No prior experience is required. React Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.

How long does the “Discriminated Unions for Component Variants” lesson take?

Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.

Can I write and run code in this React Academy lesson?

Yes. Every React Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.

All lessons in this course

  1. Discriminated Unions for Component Variants
  2. Conditional & Mapped Types in React
  3. Polymorphic Components with 'as' Prop
  4. Type-Safe Forms & API Response Contracts
← Back to React Academy