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.
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.
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} />
}
ID: code-quality-essentials.organization.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:
// 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} />
}