Introduction
The newtype pattern wraps an existing type in a single-field struct, providing type safety and API boundaries with zero runtime overhead. The compiler optimizes the wrapper away completely.
Key Concepts
Basic Newtype
ruststruct UserId(u64); struct OrderId(u64); fn get_user(id: UserId) -> User { ... } fn get_order(id: OrderId) -> Order { ... } // Compile error: cannot pass OrderId where UserId expected get_user(OrderId(42)); // Error!
Both UserId and OrderId are u64 at runtime, but the compiler prevents mixing them up.
repr(transparent)
rust#[repr(transparent)] struct Meters(f64);
repr(transparent) guarantees the newtype has the exact same memory layout as its inner type. This is essential for FFI and transmutation safety.
Real World Context
Newtypes are ubiquitous in production Rust: database IDs, validated strings, units of measurement, and API boundary types. They are the foundation of type-driven development.
Deep Dive
Implementing Traits for Newtypes
rust#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct Celsius(f64); #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] struct Fahrenheit(f64); impl Celsius { fn to_fahrenheit(self) -> Fahrenheit { Fahrenheit(self.0 * 9.0 / 5.0 + 32.0) } } impl From<Celsius> for Fahrenheit { fn from(c: Celsius) -> Self { c.to_fahrenheit() } }
Validation on Construction
ruststruct NonEmptyString(String); impl NonEmptyString { fn new(s: String) -> Option<Self> { if s.is_empty() { None } else { Some(NonEmptyString(s)) } } fn as_str(&self) -> &str { &self.0 } }
Zero Cost Verified
rustuse std::mem::size_of; assert_eq!(size_of::<u64>(), size_of::<UserId>()); // Both 8 bytes
The compiler generates identical machine code for operations on UserId and u64.
Common Pitfalls
- Forgetting
#[repr(transparent)]when the newtype is used in FFI. - Implementing
Derefto the inner type — this makes the newtype transparent to all methods, defeating the purpose. - Not deriving
Clone,Copy,Hashetc. when the inner type supports them.
Best Practices
- Use newtypes for any domain ID, measurement, or validated string.
- Derive common traits rather than implementing them manually.
- Use
repr(transparent)when FFI compatibility is needed. - Avoid
Derefto the inner type; provide explicit accessor methods instead.
Summary
Newtypes provide compile-time type safety with zero runtime overhead. The compiler erases the wrapper entirely, generating the same machine code as the inner type. Use them liberally for domain modeling.
Code Examples
use std::mem::size_of;
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UserId(u64);
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct OrderId(u64);
fn process_user(id: UserId) {
println!("Processing user {}", id.0);
}
fn main() {
let user = UserId(42);
let order = OrderId(42);
process_user(user); // OK
// process_user(order); // Compile error!
// Zero cost: same size as u64
assert_eq!(size_of::<UserId>(), size_of::<u64>());
assert_eq!(size_of::<OrderId>(), size_of::<u64>());
}