Course contentsShow
← Rust
Lesson 602 of 1,9105. Type System and TraitsFree lesson

The Typestate Pattern for API Robustness

Enforce sequence-of-operations using types that transition at compile time.

In this lesson, you will learn how to use Rust’s type system to enforce a specific sequence of operations, making invalid states impossible to represent at compile time.

The Problem: Logical Ordering

Often, we build APIs where methods must be called in a strict order. For example, a Post might need to be Drafted, then Reviewed, and finally Published. If you use a simple struct Post { state: String }, a developer could accidentally call publish() on a Draft without it being reviewed. This leads to runtime bugs.

The Solution: Typestate

The Typestate pattern moves this logic from runtime checks into the type system. Instead of one struct with a state field, we define a different type for each state. We then transition the object by consuming it (self) and returning a new instance representing the next state.

Consider this workflow:

struct Draft;
struct Published;

struct Post<State> {
    content: String,
    state: State,
}

impl Post<Draft> {
    fn new(content: String) -> Self {
        Post { content, state: Draft }
    }

    fn publish(self) -> Post<Published> {
        Post {
            content: self.content,
            state: Published,
        }
    }
}

// Post<Draft> does not have a 'delete' method, 
// but Post<Published> might!

Why this is powerful

  1. Compile-Time Safety: If you try to call publish() on a Post<Published>, the code won't compile because the method is only defined for Post<Draft>.
  2. Zero-Cost: Because these types exist only to guide the compiler, they are "zero-cost." They vanish during compilation, leaving only the machine code behind.
  3. Intentional APIs: You remove the need for Option or Result types inside your struct fields to track "if a value exists yet," because the type itself tells you exactly what is present.

By encoding the state into the type, your API becomes self-documenting. The user cannot physically perform an action that isn't allowed in the current phase of the lifecycle.

Key Takeaway: By using generic states and consuming self to transition between types, you move logical errors from runtime crashes to compile-time compiler errors.