In this lesson, you will learn how to leverage Rust's type system to shift runtime error checks into compile-time guarantees, effectively making invalid states impossible to represent.
The Problem with Runtime Validation
Often, we define structures that allow "invalid" combinations of data. Consider a NetworkRequest object. If we use an Option<Response> field, we risk calling .unwrap() or handling a None variant when we haven't actually received the data yet. This leads to defensive coding and runtime panics.
Moving Errors to the Type System
Instead of checking if a request has a response at runtime, we can encode the state of the request into its type. By using generic type parameters, we define transitions that only exist if the previous state was valid.
struct Unsent;
struct Received(String);
struct Request<State> {
state: State,
}
impl Request<Unsent> {
fn new() -> Self { Self { state: Unsent } }
// This method consumes the request and changes its type
fn send(self) -> Request<Received> {
Request { state: Received("Payload".to_string()) }
}
}
impl Request<Received> {
fn get_data(&self) -> &str {
&self.state.0
}
}
Why This Eliminates Errors
In the code above, the compiler tracks the state:
- You cannot call
get_data()on aRequest<Unsent>because that method simply doesn't exist on that type. - You cannot accidentally "receive" a response before sending the request, because the
send()function only accepts theUnsentstate.
By partitioning logic into specific types, you eliminate "logic errors" (like calling an endpoint before authentication or reading a file before opening it) at the architectural level. You no longer need to return a Result or check an is_ready boolean because the code literally will not compile if you attempt to perform an operation in an invalid state.
This pattern shifts your error-handling focus: instead of writing code to handle what happens when things go wrong, you write types that ensure things can only go right.
Key Takeaway: The Type-State pattern uses generic markers to restrict method availability, turning potential runtime failures into compile-time logic errors.