Learn · Hidden Machines · Part I
budding
Too Many Switches for One Light
Boolean flags, result types, and narrowed boundaries.
You will turn mutually exclusive flags into a sum type, expected failure into an ordinary result alternative, and repeated boundary checks into one parser that constructs a narrower value.
Three booleans make eight rooms
An HTTP view carries loading, loaded, and failed. Each Boolean doubles the state space, so the record admits eight combinations. Write down the meaning of true, true, true. Then try false, false, false. If the team has no agreed meaning for a combination, the type has admitted a room the program was never meant to enter.
The recovered carrier is a sum: one value chosen from several alternatives. In TypeScript, the tag gives the choice a name:
type RequestState<Data> =
| { readonly tag: "idle" }
| { readonly tag: "loading" }
| { readonly tag: "loaded"; readonly data: Data }
| { readonly tag: "failed"; readonly diagnostic: Diagnostic };This does more than reduce eight cases to four. It places data only where data exists and a diagnostic only where failure exists. A renderer that switches on tag receives the right payload after narrowing.
Failure takes the ordinary road
A decoder that returns null may mean absent input, malformed bytes, an unsupported version, or an internal defect. A thrown expected error is clearer only until a caller forgets which invisible exits are possible.
Expected failure is another sum:
type Result<Value, Failure> =
| { readonly tag: "ok"; readonly value: Value }
| { readonly tag: "error"; readonly error: Failure };The function becomes total over its declared input: every admitted input returns one declared alternative. “Total” does not mean success; it means the control-flow contract names every ordinary outcome.
Parse once, then carry evidence
Suppose a handler checks that request.limit is an integer between 1 and 100, then passes the same unrefined number onward. Every downstream layer must remember the check. Eventually one forgets.
A boundary parser should instead return a narrower carrier such as PageLimit. Only the smart constructor can create one. After success, the value itself is evidence that the boundary conditions held. This is often called “parse, don't validate”: validation returning the original shape has thrown away the fact it just established.
Make illegal states unconstructible at the boundary; do not make every caller remember why they are illegal.
Where the model stops
A sum type describes alternatives. It does not describe which event may move between them. If loaded may return to loading only after refresh, the program needs transitions as well as states. Nor should every pair of booleans be merged: isPinned and isMuted may be genuinely independent.
Lessons
- Contradictory booleans often encode one unnamed choice.
- Expected failure belongs in the result carrier.
- A successful boundary check should construct a narrower value once.
- Independence is the refusal test: independent facts remain a product.
Practice
- Without notes, enumerate the eight states of three booleans and mark the four request states the program intends.
- Distinguish a nullable cache lookup where absence is ordinary from a decoder where malformed input needs a diagnostic.
- Transfer the model to an elevator indicator with
movingUp,movingDown, andstopped.
What if the alternatives also have rules about movement? The next chapter adds events.
References
- “TypeScript Handbook, “Discriminated unions”.” — the language mechanism used for tagged alternatives and narrowing.
- “Alexis King, “Parse, Don't Validate”.” — the boundary discipline of preserving checked information in the output type.