Typestate Pattern: Example
The typestate pattern encodes part of a value’s runtime state into its type. This allows us to prevent invalid or inapplicable operations at compile time.
use std::fmt::Write as _; #[derive(Default)] struct Serializer { output: String, } struct SerializeStruct { serializer: Serializer, } impl Serializer { fn serialize_struct(mut self, name: &str) -> SerializeStruct { writeln!(&mut self.output, "{name} {{").unwrap(); SerializeStruct { serializer: self } } fn finish(self) -> String { self.output } } impl SerializeStruct { fn serialize_field(mut self, key: &str, value: &str) -> Self { writeln!(&mut self.serializer.output, " {key}={value};").unwrap(); self } fn finish_struct(mut self) -> Serializer { self.serializer.output.push_str("}\n"); self.serializer } } fn main() { let serializer = Serializer::default() .serialize_struct("User") .serialize_field("id", "42") .serialize_field("name", "Alice") .finish_struct(); println!("{}", serializer.finish()); }
Serializer
usage flowchart:
-
This example is inspired by Serde’s
Serializer
trait. Serde uses typestates internally to ensure serialization follows a valid structure. For more, see: https://serde.rs/impl-serializer.html -
The key idea behind typestate is that state transitions happen by consuming a value and producing a new one. At each step, only operations valid for that state are available.
-
In this example:
-
We begin with a
Serializer
, which only allows us to start serializing a struct. -
Once we call
.serialize_struct(...)
, ownership moves into aSerializeStruct
value. From that point on, we can only call methods related to serializing struct fields. -
The original
Serializer
is no longer accessible — preventing us from mixing modes (such as starting another struct mid-struct) or callingfinish()
too early. -
Only after calling
.finish_struct()
do we receive theSerializer
back. At that point, the output can be finalized or reused.
-
-
If we forget to call
finish_struct()
and drop theSerializeStruct
early, theSerializer
is also dropped. This ensures incomplete output cannot leak into the system. -
By contrast, if we had implemented everything on
Serializer
directly — as seen on the previous slide, nothing would stop someone from skipping important steps or mixing serialization flows.