Make Illegal States Unrepresentable
Design your data types so that invalid combinations of values simply can't be created, and let the compiler catch a whole class of bugs for you.
Also known as: Making Illegal States Unrepresentable, Make Impossible States Impossible, Type-Driven Design
Category: Software Development
Tags: software-design, software-development, programming, functional-programming, type-systems, domain-modeling, error-prevention, principles
Explanation
A lot of bugs come from data that should never have existed. A user marked as "active" who also has a deletion date. An order that is both "paid" and "cancelled". A connection that is "connected" and still carries a disconnect reason. The code checks for some of these cases. Rarely for all of them.
"Make illegal states unrepresentable" is the fix. Yaron Minsky (Jane Street) popularized the phrase in his talks and writing on effective OCaml. Scott Wlaschin then spread it through the F# community, and it's now standard advice in Haskell, Rust, Elm and TypeScript circles. The idea is simple. Stop writing code that checks whether data is valid, and design your types so that invalid data can't exist in the first place.
**The problem**:
Consider a connection record with three fields: `isConnected`, `connectedAt` (optional) and `disconnectReason` (optional). Nothing stops you from writing `isConnected: true` with a `disconnectReason`, or `isConnected: false` without any reason. So every function that touches this record has to check for impossible combinations. Forget one check, and you've got a bug.
**The fix**:
Model each valid state explicitly, usually with a sum type (also called a tagged union, a discriminated union or an enum with data):
- `Connecting`
- `Connected { connectedAt }`
- `Disconnected { reason }`
Now each state carries only the data that makes sense for it. You can't write the impossible combinations anymore. And the compiler forces every consumer to handle every state.
**Common techniques**:
- **Sum types** instead of boolean flags plus optional fields
- **Non-empty collections** when an empty list makes no sense (e.g., an order with zero items)
- **Value objects / newtypes** like `EmailAddress` or `PositiveInteger` instead of raw strings and numbers
- **State machines in the type system**, where each transition is a function from one state type to another
- **Smart constructors**: the only way to create a value, so its rules get enforced exactly once
**Why it's worth the effort**:
- Bugs show up at compile time instead of in production
- You can delete a LOT of defensive checks scattered across the codebase
- Your types become precise, executable documentation of the domain
- Exhaustiveness checks make sure a new state gets handled everywhere
- Refactoring gets safer, because the compiler points at every place you need to change
**Keep in mind**:
You can't encode every rule in a type system, and trying to will make your code hard to read. Start with the invariants that cause the most bugs. Validate the rest at clear boundaries (see Parse, Don't Validate).
This is the Pit of Success applied to data modeling: when invalid states are impossible to express, correct code is the only code you can write.
Related Concepts
← Back to all concepts