🎓 A1Learner
🔍

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?