Newtype Pattern
The Newtype pattern wraps an existing type inside a single-field tuple struct (pub struct UserId(u64); or #[repr(transparent)] pub struct Meters(pub f64);) to create a distinct, compiler-enforced type at compile time with zero memory or CPU overhead at runtime.
Without Newtypes (Primitive Obsession Bug!):
`fn transfer(from_account: u64, to_account: u64, amount_cents: u64)`
Caller writes: `transfer(amount, from_id, to_id)` ---> Compiles silently! Catastrophic bug!
With Newtypes (Compile-Time Domain Safety + Zero Runtime Cost):
`fn transfer(from: AccountId, to: AccountId, amount: Cents)`
Caller writes: `transfer(amount, from_id, to_id)` ---> Compile Error: expected `AccountId`, found `Cents`!
Four Core Uses of the Newtype Pattern in Rust
| Use Case | Example | Why a Type Alias (type Foo = u64;) Is Not Enough |
|---|---|---|
| 1. Preventing Unit / ID Mix-Ups | struct UserId(u64) vs struct OrderId(u64) |
A type alias is just a synonym—rustc allows passing OrderId where UserId is expected if they are type aliases! |
| 2. Enforcing Construction Invariants ("Parse, Don't Validate") | struct NonEmptyStr(String) with private field .0 |
Guarantees that any NonEmptyStr instance in the program was validated at construction time (try_new). |
| 3. Bypassing the Orphan Rule | struct PrettyVec<T>(pub Vec<T>) implementing fmt::Display |
Rust's coherence rules forbid implementing foreign trait Display on foreign type Vec<T> directly; wrapping it in a local newtype makes the type local! |
| 4. Hiding / Restricting Inner Trait Impls | struct SecretToken(String) omitting Display and redacting Debug |
Prevents accidental logging of sensitive credentials while still holding a String internally. |
Enforcing Domain Invariants ("Parse, Don't Validate") and Bypassing the Orphan Rule
By keeping the inner tuple field private (struct EmailAddress(String); without pub on String), external modules can only obtain an EmailAddress through your validated EmailAddress::parse(...) constructor. Any downstream function accepting email: &EmailAddress never needs to re-check whether the string contains '@'!
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct EmailAddress(String); // Private inner field enforces invariant!
#[derive(Debug, PartialEq, Eq)]
pub enum EmailError {
MissingAtSymbol,
EmptyLocalOrDomain,
}
impl EmailAddress {
pub fn parse(raw: impl Into<String>) -> Result<Self, EmailError> {
let s = raw.into();
let (local, domain) = s.split_once('@').ok_or(EmailError::MissingAtSymbol)?;
if local.is_empty() || domain.is_empty() || !domain.contains('.') {
return Err(EmailError::EmptyLocalOrDomain);
}
Ok(Self(s))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
/// Implementing foreign trait `fmt::Display` on a newtype wrapping foreign type `Vec<T>`
/// (which would violate the Orphan Rule if implemented on `Vec<T>` directly!):
#[repr(transparent)]
pub struct CsvRow<T>(pub Vec<T>);
impl<T: fmt::Display> fmt::Display for CsvRow<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
for (i, item) in self.0.iter().enumerate() {
if i > 0 {
f.write_str(", ")?;
}
write!(f, "{item}")?;
}
Ok(())
}
}
fn main() {
let valid = EmailAddress::parse("ada@example.org").unwrap();
assert_eq!(valid.as_str(), "ada@example.org");
assert_eq!(
EmailAddress::parse("invalid-email"),
Err(EmailError::MissingAtSymbol)
);
let row = CsvRow(vec![10, 20, 30, 40]);
println!("Validated email: {}, formatted CsvRow: [{}]", valid.as_str(), row);
}
Validated email: ada@example.org, formatted CsvRow: [10, 20, 30, 40]
Selective Trait Forwarding vs Deref
Avoid implementing Deref<Target = Inner> on a domain newtype like UserId(u64) or Meters(f64) if the whole point of the newtype is to restrict operations (for example, multiplying UserId * UserId makes no sense). Instead, derive only the specific traits that make domain sense (#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]) and implement AsRef<str> or explicit accessor methods (.get(), .as_str()).