Newtype Pattern
The newtype pattern wraps an existing type inside a single-field tuple struct (struct Wrapper(Inner); or #[repr(transparent)] struct Wrapper(Inner);) to create a distinct nominal type at compile time with zero runtime memory or performance overhead.
Four Core Reasons to Use a Newtype
| Use Case | Example | What the Newtype Achieves |
|---|---|---|
| 1. Prevent unit / ID mix-ups | struct UserId(u64); vs. struct OrderId(u64); |
Passing an OrderId to find_user(id: UserId) is a compile-time error (E0308) |
| 2. Enforce domain validation invariants | struct EmailAddress(String); |
Private inner field ensures an EmailAddress can only be created via a validating constructor |
3. Bypass the orphan rule (E0117) |
struct PrettyVec<T>(Vec<T>); |
Allows implementing a foreign trait (fmt::Display) on a wrapper around a foreign type (Vec<T>) |
| 4. Customize trait behavior | std::cmp::Reverse<T>, std::num::Wrapping<T> |
Overrides Ord, Add, or Debug without altering the underlying type's default behavior |
Zero-Cost Representation (#[repr(transparent)])
At runtime, a single-field struct struct Meters(f64); occupies the exact same 8 bytes on the stack or in registers as a bare f64. Adding #[repr(transparent)] further guarantees at the ABI level that Meters is passed in the exact same CPU registers across FFI boundaries as f64:
use std::fmt;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct UserId(pub u64);
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[repr(transparent)]
pub struct AccountId(pub u64);
// Bypassing the orphan rule to implement `fmt::Display` on a `Vec<String>` wrapper:
pub struct CommaList(pub Vec<String>);
impl fmt::Display for CommaList {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "[{}]", self.0.join(", "))
}
}
fn fetch_user(user: UserId) {
println!("Fetching user #{}", user.0);
}
fn main() {
let uid = UserId(101);
let _aid = AccountId(101);
fetch_user(uid);
// `fetch_user(_aid)` is rejected at compile time with E0308!
let list = CommaList(vec![String::from("rust"), String::from("types")]);
println!("Formatted CommaList: {list}");
println!(
"size_of::<UserId>() = {} bytes (same as u64: {})",
size_of::<UserId>(),
size_of::<u64>()
);
}
Fetching user #101
Formatted CommaList: [rust, types]
size_of::<UserId>() = 8 bytes (same as u64: 8)
Selective Trait Forwarding vs. Deref
A newtype intentionally hides the traits and methods of its inner type unless you explicitly derive or implement them (for example, UserId(u64) does not derive Add because adding two user IDs together is meaningless). When a newtype represents a transparent smart-pointer-like wrapper around a slice or buffer, implementing AsRef<Inner> or Deref<Target = Inner> selectively exposes read-only methods of the inner type.