Independent booleans mint illegal states that a single enum would forbid
Aliases: flag soup · isLoading isError · mutually exclusive enum
What it is
isLoading, isError, isSuccess, isEmpty are independently flippable switches. Four switches give sixteen combinations; four or five are legal, the rest illegal. A mutually exclusive enum folds the same fact into one value: idle | loading | success | error | empty, combination space equal to the node count. Illegal states “often” appear not because programmers enjoy dirty data, but because independent booleans are the default, handy model, and the space is already a superset at the declaration.
This leaf names that modelling habit. Once the space is a superset, illegal will appear — a structural consequence; what to change here is the habit itself.
Why it happens
A boolean’s temptation is local: this beat needs “loading”, so add isLoading. The next beat needs failure, so add isError. Each flag is true in its own story; exclusion among stories has no type to enforce it. Assignments happen separately: loading starts, loading goes true; the success callback sets success true and forgets to set loading false; loading ∧ success is born. Worse under concurrency: failure and success callbacks each write one flag, and arrival order picks which illegal.
An enum writes exclusion into the assignment: the beat that writes success cannot still be loading. That is not style. It folds sixteen cells to five, and “forgot to clear the flag” becomes unrepresentable at the type. Migrating from booleans to an enum forces UI branches that used to guess “which flag do we look at first” into exhaustive match — which is exactly when hidden illegals surface.
Where it stops holding
Truly independent facts should be booleans: a notification switch and dark mode do not exclude. Folding them into an enum is the other mistake. Two orthogonal small machines (“sync state” and “edit state”) should be two enums, not one twelve-value enum, and not six or seven booleans. A triple (on / off / indeterminate) is not two booleans (checked and indeterminate) — another classic illegal source; use one three-valued type. When a historical API only offers booleans, the adapter should still fold to an enum inside and not pass the boolean soup into the UI. Bitmasks as “space-saving boolean clusters” are the same superset, only harder to read.
Applying it
- Flags that describe which beat the same object is on fold into one enum. Forbid
isLoadingandisErrorstanding side by side as sources of truth. - UI branches exhaustively match the enum. Do not write a slide of
if (isLoading)elseif (isError)— the slide leaves combinations uncovered. - Two booleans “that should not both be true in theory” merge at once. Do not add a third boolean to “explain a bit more”.
- How to check: in a success callback, deliberately leave
isLoadingtrue (or, on the enum version, try to assign two members at once). The boolean version should be able to render “succeeded and still spinning”; the enum version should fail to compile or crash at the assignment. Then count everyisXxxin the product, pairwise “can both be true”: each pair whose answer is “should not, but the type allows” is an enum waiting.
Related
- Same group: I3.13.1 Every legal transition must have a defined outcome · I3.13.2 If the data model can hold an illegal combination, it will · I3.13.4 The UI must be a projection of the state machine, not a visual guess
- Nearby: I3.01 Visibility of system status · I2.08 Load failure
- Search terms:
boolean flags·discriminated union·isLoading isError