The newtype pattern
Intermediate · Abstractions
What & why
UserId and OrderId might both just be a u64 under the hood — but a u64 is a u64 to the compiler, so nothing stops you from accidentally calling charge(order_id, user_id) with the arguments swapped. The newtype pattern wraps an existing type in a one-field tuple struct — struct UserId(u64); — to create a genuinely distinct type at compile time, for free at runtime. It’s how Rust gets type-safe units (Meters vs Feet), prevents ID mix-ups, and even works around a rule that would otherwise block you from implementing a trait you don’t own on a type you don’t own.
The idea, slowly
A one-field wrapper is a brand new type
A tuple struct with a single field is just a label the compiler now enforces:
struct Meters(f64);
struct Feet(f64);
fn main() {
let height = Meters(1.8);
let track_length = Feet(400.0);
// Both are "just f64" underneath, but they are NOT interchangeable:
// let mixed: Meters = track_length; // compile error: expected Meters, found Feet
println!("{} meters, {} feet", height.0, track_length.0);
}
At runtime Meters(1.8) is exactly one f64 in memory — the wrapper costs nothing (this is called a “zero-cost abstraction”). At compile time, though, Meters and Feet are unrelated types. Pass a Feet where a Meters is expected and you get a type error immediately, not a silently wrong distance calculation three functions later.
Preventing mixed-up IDs
This is the newtype pattern’s bread-and-butter use case. Two IDs backed by the same primitive type are easy to swap by accident — the newtype makes that swap a compile error instead of a runtime bug:
struct UserId(u64);
struct OrderId(u64);
fn charge(user: UserId, order: OrderId) {
println!("charging user #{} for order #{}", user.0, order.0);
}
fn main() {
// charge(OrderId(1), UserId(2)); // compile error: arguments in the wrong order
charge(UserId(2), OrderId(1)); // correct order, and the compiler checked it
}
Without the wrapper, both parameters would just be u64, and swapping them compiles fine — it’s a bug that only shows up when the wrong user gets charged for the wrong order. With UserId and OrderId as distinct types, the compiler catches the swap at the call site, every time.
The orphan rule, and how wrapping sidesteps it
Rust has a rule (the “orphan rule”) that blocks you from implementing a trait for a type when you own neither the trait nor the type. This exists to prevent two different crates from both implementing the same foreign trait for the same foreign type in incompatible ways. Concretely: you can’t write impl std::fmt::Display for Vec<String> in your own crate, because you own neither Display (that’s std’s) nor Vec (also std’s).
Wrapping the foreign type in a newtype you define fixes this — now you own the type (the wrapper), even though the trait is still foreign:
use std::fmt;
struct Names(Vec<String>);
impl fmt::Display for Names {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{}", self.0.join(", "))
}
}
fn main() {
let names = Names(vec!["Alice".to_string(), "Bob".to_string()]);
println!("{}", names); // Alice, Bob
}
What the compiler is thinking: the orphan rule only cares about the type in the impl ... for TYPE slot. Names is a type defined right here in this crate, so impl fmt::Display for Names is allowed — even though Display itself comes from std and the data inside is still a Vec<String>.
Getting at the inner value
A newtype doesn’t automatically inherit the wrapped type’s methods — Meters doesn’t gain f64’s methods just by wrapping one. You reach in with .0 (tuple structs index their single field as .0):
struct Meters(f64);
fn main() {
let d = Meters(42.0);
let doubled = Meters(d.0 * 2.0); // reach in with .0, then rewrap
println!("{}", doubled.0);
}
For a wrapper you want to feel more like the type it holds (e.g. calling String methods directly on a newtype around String), implement Deref so . auto-forwards to the inner value — or implement From/Into so converting between the wrapper and the inner type is a clean .into() instead of manual .0 plumbing everywhere.
Common mistakes
- Overusing newtypes for every primitive. Wrapping every
u64andStringin the codebase adds.0noise everywhere and slows readers down. Reach for a newtype when mixing two values up would be a real bug (IDs, units, currencies) — not reflexively. - Expecting inherited methods.
struct Meters(f64)does not gainf64::sqrt()or arithmetic operators automatically. You need.0to get at the inner value, or explicit trait impls (Deref,Add, …) to forward behavior. - Forgetting the newtype has no
Display/Debugby default. Printing a bareMeters(1.8)with{}fails to compile until you either#[derive(Debug)](for{:?}) or implementDisplayyourself (for{}) — wrapping a printable type doesn’t make the wrapper printable. - Reaching for a newtype when the orphan rule isn’t actually the problem. If you own the type already, just
impl Trait for YourTypedirectly — no wrapper needed. Newtypes solve orphan-rule blocks and type confusion, not every design problem.
More examples
Preventing unit mix-ups in a function signature
A race-length calculator should only ever see meters — wrapping feet in their own type forces the conversion to happen explicitly, instead of silently dividing meters by feet.
struct Meters(f64);
struct Feet(f64);
impl Feet {
fn to_meters(&self) -> Meters {
Meters(self.0 * 0.3048)
}
}
fn track_length_in_laps(track: Meters, lap_length: Meters) -> f64 {
track.0 / lap_length.0
}
fn main() {
let track = Meters(1600.0);
let lap = Feet(400.0).to_meters(); // must convert explicitly, no silent mixing
println!("{:.1} laps", track_length_in_laps(track, lap));
}
Working around the orphan rule for Display
Vec<String> isn’t yours and Display isn’t yours either, so impl Display for Vec<String> won’t compile — wrap the tags in a newtype you own, and the impl block is suddenly allowed.
use std::fmt;
struct Tags(Vec<String>);
impl fmt::Display for Tags {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let hashtags: Vec<String> = self.0.iter().map(|t| format!("#{t}")).collect();
write!(f, "{}", hashtags.join(" "))
}
}
fn main() {
let post_tags = Tags(vec!["rust".to_string(), "programming".to_string(), "learning".to_string()]);
println!("{post_tags}"); // #rust #programming #learning
}
Validating on the way in
An Email newtype whose only constructor checks the format means every Email value that exists anywhere in the program has already been validated — there’s no separate “unchecked string” path to forget to call.
struct Email(String);
impl Email {
fn new(raw: &str) -> Result<Email, String> {
if raw.contains('@') && raw.contains('.') {
Ok(Email(raw.to_string()))
} else {
Err(format!("'{raw}' doesn't look like an email address"))
}
}
}
fn main() {
for candidate in ["alice@example.com", "not-an-email"] {
match Email::new(candidate) {
Ok(e) => println!("valid: {}", e.0),
Err(msg) => println!("invalid: {msg}"),
}
}
}
Letting the compiler catch a swapped argument
SkuId and WarehouseId are both u32 underneath, but as distinct types the compiler rejects a call that passes them in the wrong order — the bug is caught before the code ever runs.
struct SkuId(u32);
struct WarehouseId(u32);
fn restock(sku: SkuId, warehouse: WarehouseId, quantity: u32) {
println!("adding {quantity} units of sku #{} to warehouse #{}", sku.0, warehouse.0);
}
fn main() {
// restock(WarehouseId(7), SkuId(4021), 50); // compile error: arguments swapped
restock(SkuId(4021), WarehouseId(7), 50); // correct order, compiler-checked
}
Your turn
This function tries to print a Meters value directly. It doesn’t compile:
struct Meters(f64);
fn describe_distance(m: Meters) {
println!("distance: {} meters", m);
}
fn main() {
let d = Meters(42.0);
describe_distance(d);
}
Show solution
The error is Meters doesn't implement std::fmt::Display (or {:?} isn’t implemented either, since there’s no derive(Debug)). Wrapping an f64 in Meters does not make Meters itself printable — the newtype is a brand new type with no methods or trait impls of its own by default. Reach into the wrapper with .0 to get the printable f64 back out:
struct Meters(f64);
fn describe_distance(m: Meters) {
println!("distance: {} meters", m.0); // .0 gets the inner f64
}
fn main() {
let d = Meters(42.0);
describe_distance(d); // distance: 42 meters
}
(Alternatively, #[derive(Debug)] on Meters and printing with {:?} would also compile — but it prints Meters(42.0), not a clean 42. Implementing Display by hand gives full control over the format.)
Quick check
Remember this
struct Meters(f64);creates a type distinct fromf64at compile time, at zero cost at runtime.- A newtype prevents accidentally passing a
UserIdwhere anOrderIdis expected, even though both are the same primitive underneath. - The orphan rule blocks
impl ForeignTrait for ForeignType— wrappingForeignTypein a newtype you own sidesteps it, because you now own the type in theimplslot. - Access the inner value with
.0, or implementDeref/Fromto make the wrapper more ergonomic to use.
Go deeper
- Rust Book - Using the Newtype Pattern — Newtype and the orphan rule.
Next: