# Discriminated unions used

- **Pattern:** `ab-002156` (`code-quality-essentials.organization.discriminated-unions`)
- **Severity:** medium
- **Lifecycle:** active
- **Last modified:** 2026-04-18
- **Canonical URL:** https://auditbuffet.com/patterns/ab-002156
- **License:** CC-BY-4.0 — attribute to AuditBuffet Pattern Catalog (https://auditbuffet.com/patterns/ab-002156)

## Why it matters

Optional boolean flags for mutually exclusive states — `isLoading`, `isError`, `isSuccess` on the same object — allow impossible combinations that the type system cannot prevent: `{ isLoading: true, isSuccess: true }` is a valid TypeScript value, and code that doesn't defensively check every combination will produce undefined behavior. ISO 25010:2011 §6.5.4 classifies this as a reliability defect. Discriminated unions encode the constraint that only one state can be active at a time directly in the type, eliminating an entire class of conditional-logic bugs.

## Severity rationale

Medium because optional boolean state flags allow impossible combinations that TypeScript cannot detect, producing runtime behavior that depends on which flag the developer happened to check first.

## Remediation

Replace multi-flag state types with a discriminated union keyed on a `status` or `type` field. TypeScript then exhaustively checks all cases at switch sites:

```typescript
// Bad: flags allow impossible combinations
type FetchState = {
  isLoading?: boolean
  isError?: boolean
  data?: User
  error?: string
}

// Good: only one state possible at a time
type FetchState =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: User }
  | { status: 'error'; error: string }

switch (state.status) {
  case 'idle': return <IdleView />
  case 'loading': return <Spinner />
  case 'success': return <UserView user={state.data} />
  case 'error': return <ErrorView message={state.error} />
}
```

## Detection

- **ID:** `discriminated-unions`
- **Severity:** `medium`
- **What to look for:** Look at state type definitions and action/event types in the codebase. A discriminated union uses a common literal type field (typically named `type`, `kind`, `status`, or `variant`) to differentiate between mutually exclusive cases. The alternative anti-pattern is using multiple optional boolean flags: `isLoading?: boolean; isError?: boolean; isSuccess?: boolean` on the same type. The optional-flags approach creates impossible states (e.g., `isLoading: true` and `isSuccess: true` simultaneously) and requires defensive checking at every usage site. Look for: form state types, async loading state types, modal/dialog state types, API response union types. These are the typical places where discriminated unions improve safety significantly.
- **Pass criteria:** Enumerate all relevant code locations. State types and async result types use discriminated unions with a `type` or `status` discriminant field rather than optional boolean flags for mutually exclusive states with at least 1 verified location.
- **Fail criteria:** State types use multiple optional boolean flags (`isLoading`, `isError`, `isSuccess`) for what should be mutually exclusive states.
- **Skip (N/A) when:** Project has no complex state types (trivially simple CRUD with no async states modeled in TypeScript).
- **Detail on fail:** `"State types use optional boolean flags instead of discriminated unions; impossible states representable in the type system"`
- **Remediation:** Model mutually exclusive states as discriminated unions:

  ```typescript
  // Bad: flags allow impossible combinations
  type FetchState = {
    isLoading?: boolean
    isError?: boolean
    data?: User
    error?: string
  }

  // Good: discriminated union — only one state possible at a time
  type FetchState =
    | { status: 'idle' }
    | { status: 'loading' }
    | { status: 'success'; data: User }
    | { status: 'error'; error: string }

  // TypeScript now exhaustively checks all cases:
  switch (state.status) {
    case 'idle': return <IdleView />
    case 'loading': return <Spinner />
    case 'success': return <UserView user={state.data} />
    case 'error': return <ErrorView message={state.error} />
  }
  ```

## External references

- iso-25010 maintainability.modifiability

Taxons: code-quality

HTML version: https://auditbuffet.com/patterns/ab-002156
