Rust Type State Pattern
The type state pattern encodes a value's stage in its type. Each stage is a separate type, like Post<Draft> and Post<Published>.
States are types
Draft and Published are empty marker structs. Post<State> carries one in a PhantomData field — zero bytes of run-time state.
struct Draft;
struct Published;
struct Post<State> {
text: String,
state: PhantomData<State>,
}A transition lives on one state only
publish exists on Post<Draft>. It consumes the draft and hands back a different type.
impl Post<Draft> {
fn publish(self) -> Post<Published> {
Post { text: self.text, state: PhantomData }
}
}Some methods exist only at the end
show is written for Post<Published> alone. A draft has no show method — calling it is error E0599.
impl Post<Published> {
fn show(&self) -> &str {
&self.text
}
}The journey
Each step returns a new type, so the variable's type tells you exactly where the post stands.
let post = Post::<Draft>::new("hello rust");
let post = post.publish();
println!("{}", post.show());Example
use std::marker::PhantomData;
struct Draft;
struct Reviewed;
struct Published;
struct Post<State> {
text: String,
state: PhantomData<State>,
}
impl Post<Draft> {
fn new(text: &str) -> Post<Draft> {
Post { text: text.to_string(), state: PhantomData }
}
fn review(self) -> Post<Reviewed> {
Post { text: self.text, state: PhantomData }
}
}
impl Post<Reviewed> {
fn publish(self) -> Post<Published> {
Post { text: self.text, state: PhantomData }
}
}
impl Post<Published> {
fn show(&self) -> &str {
&self.text
}
}
fn main() {
let post = Post::<Draft>::new("rust patterns");
let post = post.review();
let post = post.publish();
println!("live: {}", post.show());
}Key points
- Type state encodes a stage in a type.
- The stage is a stage of a value's life.
- Post<Draft> and Post<Published> are different types.
- They are not one type with a flag
- Use one marker struct per state.
- Each marker is zero-sized.
- Store it in a PhantomData field.
- The state costs nothing at run time
- Write the impl block for one state at a time.
- Each state offers exactly the right methods.
- It offers the moves that make sense for it
- A transition method takes self.
- It returns the value wearing its next state.
- So the old state cannot be used again.
- It cannot be used again by accident
In this example, a post is born as a Post<Draft> holding the text "rust patterns". review consumes the draft. It returns a Post<Reviewed>. publish consumes that. It returns a Post<Published>. Only the published type has a show method, so the final line can print the text. Try to call show one step earlier and the program stops compiling — the method simply is not there.
Note
There is no flag to check.
There is no wrong order to defend against at run time.
A skipped step calls a method that does not exist.
The compiler reports it.
It reports it like any other missing method
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
In the type state pattern, what stops a draft post from being published without a review?