Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Typestate and State Machines

Rule

Use typestate when compile-time transition enforcement materially protects an important workflow invariant; otherwise use runtime enums or validated structs.

Why

Typestate prevents invalid ordering but exposes state in type signatures and makes storage, persistence, and evolution more complex.

Activation

Apply this page to ordered workflows such as connected-to-authenticated or configured-to-started. Skip ordinary optional configuration.

Do

  • Use small marker types for compile-time states.
  • Store shared data in one generic Workflow<State> type.
  • Put consuming transitions on the source state.
  • Put state-independent accessors on impl<State>.
  • Use PhantomData<State> when state has no runtime data.
  • Use runtime enums for persisted, networked, user-selected, or heterogeneous state.
  • Keep external validation even when internal transitions use typestate.

Avoid

  • Do not use typestate for UI labels or reporting states.
  • Do not encode runtime data as type parameters.
  • Do not create markers for unclear or frequently changing workflows.
  • Do not force typestate through persistence, queues, or async task boundaries.
  • Do not use typestate when a fallible constructor or runtime enum is clearer.

Public API Notes

Published typestate APIs expose workflow structure to callers. Keep state names clear and consider sealed markers when the state set may evolve.

Example

#![allow(unused)]
fn main() {
use std::marker::PhantomData;

pub struct Draft;
pub struct Published;

pub struct Article<State> {
    body: String,
    state: PhantomData<State>,
}

impl Article<Draft> {
    pub fn new(body: impl Into<String>) -> Self {
        Self {
            body: body.into(),
            state: PhantomData,
        }
    }

    pub fn publish(self) -> Article<Published> {
        Article {
            body: self.body,
            state: PhantomData,
        }
    }
}
}

Exceptions

  • Use typestate builders when missing a required step would create a dangerous or invalid operation.
  • Accept additional complexity in safety-sensitive protocols where invalid ordering must not compile.