Parse, Don’t Validate

The newtype pattern can be leveraged to enforce invariants.

#![allow(unused)]
fn main() {
pub struct Username(String);

impl Username {
    pub fn new(username: String) -> Result<Self, InvalidUsername> {
        if username.is_empty() {
            return Err(InvalidUsername::CannotBeEmpty)
        }
        if username.len() > 32 {
            return Err(InvalidUsername::TooLong { len: username.len() })
        }
        // Other validation checks...
        Ok(Self(username))
    }

    pub fn as_str(&self) -> &str {
        &self.0
    }
}
pub enum InvalidUsername {
    CannotBeEmpty,
    TooLong { len: usize },
}
}
  • The newtype pattern, combined with Rust’s module and visibility system, can be used to guarantee that instances of a given type satisfy a set of invariants.

    In the example above, the raw String stored inside the Username struct can’t be accessed directly from other modules or crates, since it’s not marked as pub or pub(in ...). Consumers of the Username type are forced to use the new method to create instances. In turn, new performs validation, thus ensuring that all instances of Username satisfy those checks.

  • The as_str method allows consumers to access the raw string representation (e.g., to store it in a database). However, consumers can’t modify the underlying value since &str, the returned type, restricts them to read-only access.

  • Type-level invariants have second-order benefits.

    The input is validated once, at the boundary, and the rest of the program can rely on the invariants being upheld. We can avoid redundant validation and “defensive programming” checks throughout the program, reducing noise and improving performance.