Model an async request's state so impossible states are unrepresentable, and TS narrows correctly.
The single most valuable TS pattern for UI: making illegal states impossible to construct.
Give each variant a common literal 'tag' field (here status). A switch on the tag narrows the type in each branch — inside case 'success' TS KNOWS data exists; inside case 'error' it knows error exists and data does NOT. This beats a bag of optional fields (isLoading?, data?, error?) where you can accidentally represent 'loading AND error' — a bug the union makes uncompilable. This is 'make illegal states unrepresentable' applied to everyday UI: model the four request phases as { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: E } and the compiler forces every consumer to handle each phase and only lets them touch fields that actually exist in that phase. Pair it with an exhaustiveness check (assign the switch value to a never in default) so adding a fifth state is a compile error until you handle it. The tag can be any literal type — string, number, or boolean — as long as every member has it and the values are distinct.
Any request/reducer/state machine. Redux action types are discriminated unions on `type`. Pairs with exhaustiveness checking via never.