In this lesson, you will learn how to leverage the Rust type system to guarantee that data is valid by construction, moving validation logic from runtime checks to compile-time guarantees.
The Problem with Runtime Checks
Usually, when we accept raw input (like a string or a JSON object), we perform "defensive programming": we check if a string is empty, if a number is within a range, or if an email address has an "@" symbol. This often leads to scattered if statements throughout your business logic, leaving you constantly wondering if a variable has actually been validated yet.
Moving Logic to the Type System
Instead of passing around raw data, we can define a trait that enforces a validation requirement for any type that represents "checked" data. By using a generic wrapper, we ensure that a function can only accept data that has already passed through a validator.
Consider this pattern:
pub trait Validator<T> {
fn validate(input: T) -> Result<T, String>;
}
// A generic wrapper that only holds validated data
pub struct Validated<T, V: Validator<T>> {
data: T,
_marker: std::marker::PhantomData<V>,
}
impl<T, V: Validator<T>> Validated<T, V> {
pub fn new(input: T) -> Result<Self, String> {
let validated_data = V::validate(input)?;
Ok(Self { data: validated_data, _marker: std::marker::PhantomData })
}
pub fn into_inner(self) -> T { self.data }
}
By defining a specific Validator struct for a type—for example, an EmailValidator that implements Validator<String>—we can require functions to accept Validated<String, EmailValidator> instead of a plain String.
When you write a function signature like fn send_email(address: Validated<String, EmailValidator>), the compiler becomes your first line of defense. You no longer need to write error handling for an invalid email inside send_email because it is physically impossible to construct that type without passing through the validator. This "Type-Driven" approach eliminates the state of "uncertainty" in your application's data flow.
Key Takeaway: By encoding validation requirements into generic types, you shift error handling from a runtime burden to a structural requirement enforced by the compiler.