Typestate: State Machines That Compile
How do we prevent a method from being called in the wrong state? Take the enum idea one step further: give each state its own type, and make transitions consume the old state.
pub struct Unarmed(SafetyMonitor);
pub struct Armed(SafetyMonitor);
impl Unarmed {
pub fn arm(self, checks: PreflightReport) -> Result<Armed, ArmError> {
todo!() // consumes self; the Unarmed value is gone on success
}
}
impl Armed {
pub fn trigger(&mut self) -> Action { todo!() }
pub fn disarm(self) -> Unarmed { todo!() }
}
There’s no way to call trigger on an unarmed monitor. The method
doesn’t exist on that type, so the mistake is caught while the code is
being generated rather than when it runs.
When to use it:
| Use typestate when | Use a plain enum when |
|---|---|
| Wrong-state calls are costly (safety, money, data loss) | States change often at runtime and are inspected together |
| The state graph is small and stable | The graph is large or data-driven |
| Compile-time proof is worth some API weight | Mixed states must live in one collection |
Builders with required fields are a common everyday form of this pattern:
build() only exists once the required fields are set.