Type System Fundamentals

Type Safety and API Design

Type-driven API design ("parse, don't validate") uses Rust's type system to turn runtime preconditions and boolean flags into compile-time types so callers are guided toward correct usage by their editor and the compiler.

1. Replace Boolean Parameters with Descriptive Enums

A function signature like fn connect(host: &str, verify_tls: bool, keep_alive: bool) is error-prone at call sites (connect("example.com", true, false)—which bool is which?). Replacing boolean flags with two-variant domain enums makes call sites self-documenting and impossible to transpose:

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TlsPolicy {
    VerifyCertificates,
    DangerAcceptInvalidCerts,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum KeepAlive {
    Enabled,
    Disabled,
}

pub fn connect(host: &str, tls: TlsPolicy, keep_alive: KeepAlive) {
    println!("Connecting to {host} (tls={tls:?}, keep_alive={keep_alive:?})");
}

fn main() {
    connect(
        "db.internal",
        TlsPolicy::VerifyCertificates,
        KeepAlive::Enabled,
    );
}
Connecting to db.internal (tls=VerifyCertificates, keep_alive=Enabled)

2. "Parse, Don't Validate": Return Proof Types Instead of bool

A validation function fn is_valid_username(s: &str) -> bool checks an input and immediately throws away the proof in the type system, forcing downstream functions to either trust callers blindly or re-validate s: &str.

Instead, parse raw input into a validated newtype (Username) whose constructor is the only way to create an instance:

mod auth {
    #[derive(Debug, Clone, PartialEq, Eq)]
    pub struct Username(String); // Private inner field!

    impl Username {
        pub fn parse(raw: &str) -> Result<Self, &'static str> {
            let trimmed = raw.trim();
            if (3..=20).contains(&trimmed.len())
                && trimmed.chars().all(|c| c.is_ascii_alphanumeric() || c == '_')
            {
                Ok(Self(trimmed.to_owned()))
            } else {
                Err("username must be 3..=20 ASCII alphanumeric/underscore characters")
            }
        }

        pub fn as_str(&self) -> &str {
            &self.0
        }
    }
}

// Downstream functions accept `auth::Username` and NEVER need to re-validate it:
fn create_account(user: &auth::Username) {
    println!("Created account for verified user '{}'", user.as_str());
}

fn main() {
    let valid = auth::Username::parse("prime_dev").unwrap();
    create_account(&valid);

    let bad = auth::Username::parse("a!");
    println!("Rejected bad username: {bad:?}");
}
Created account for verified user 'prime_dev'
Rejected bad username: Err("username must be 3..=20 ASCII alphanumeric/underscore characters")

3. Non-Zero and Non-Empty Types (NonZero<T>)

When 0 is not a valid domain value (such as page sizes, worker thread counts, divisor arguments, or database IDs), use std::num::NonZeroUsize (or NonZeroU32 / NonZeroU64) in your API signature. This both eliminates divide-by-zero / zero-step bugs at compile time and enables the niche optimization so Option<NonZeroU64> occupies just 8 bytes.

Summary of Type-Safe API Idioms

Weakly Typed Anti-Pattern Type-Safe Replacement
fn transfer(from: u64, to: u64, cents: u64) fn transfer(from: AccountId, to: AccountId, amount: UsdCents)
fn render(visible: bool, cached: bool) fn render(vis: Visibility, cache: CachePolicy)
fn set_workers(n: usize) (panics if n == 0) fn set_workers(n: NonZeroUsize)
struct Response { status: u16, body: Option<String>, err: Option<String> } enum Response { Ok(String), Err { status: u16, message: String } }
#[must_use] omitted on a builder/guard return type Annotate with #[must_use] so ignored return values trigger a compiler warning