Discriminated unions used
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:
// 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, orvariant) to differentiate between mutually exclusive cases. The alternative anti-pattern is using multiple optional boolean flags:isLoading?: boolean; isError?: boolean; isSuccess?: booleanon the same type. The optional-flags approach creates impossible states (e.g.,isLoading: trueandisSuccess: truesimultaneously) 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
typeorstatusdiscriminant 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:
// 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:2011 · maintainability.modifiability — Maintainability — Modifiability
Taxons
History
- 2026-04-18·v1.0.0·Initial import from code-quality-essentials·automated