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

Encoding Preconditions in Type Signatures

Require proof of previous state changes through phantom generic parameters.

In this lesson, you will learn how to use phantom generic parameters to ensure that specific operations can only occur after previous state changes have been proven at compile time.

The Problem: Illegal State Transitions

Often, we have objects that must go through a sequence of initialization steps before they are "ready." For example, a Document might need to be Validated before it can be Published. If we use simple boolean flags, the compiler won't stop us from calling .publish() on an unvalidated document; we’d only find out about the error at runtime.

The Solution: Phantom Types as State Proofs

We can "lock" our data behind a type signature that requires a specific generic parameter. We define marker types to represent states, then use PhantomData to store the "proof" that the object is in that state.

use std::marker::PhantomData;

// Marker types for states
struct Raw;
struct Validated;

struct Document<State> {
    content: String,
    _marker: PhantomData<State>,
}

impl Document<Raw> {
    fn new(content: String) -> Self {
        Self { content, _marker: PhantomData }
    }

    fn validate(self) -> Document<Validated> {
        Document {
            content: self.content,
            _marker: PhantomData,
        }
    }
}

impl Document<Validated> {
    fn publish(self) {
        println!("Publishing: {}", self.content);
    }
}

Why This Works

By defining the publish method only for Document<Validated>, the Rust compiler physically prevents you from calling it on a Document<Raw>. If you try to pass an unvalidated document, you get a type mismatch error during compilation.

The PhantomData<State> field doesn't actually take up any memory space; it exists solely for the type checker. It acts as a "proof" that the transition occurred. Because our methods consume self and return a new instance with a different type parameter, we enforce an irreversible flow: you must pass through validate to obtain the type required for publish. This effectively encodes your application’s business logic directly into the type system.

Key Takeaway: By binding methods to specific phantom generic states, you turn logical programming errors into compile-time type errors.