The Newtype Pattern
Wrap an existing type in a thin struct to create a distinct type with its own invariants, trait implementations, and API without runtime overhead.
Recipe
#[repr(transparent)]
struct UserId(u64);
impl UserId {
pub fn new(id: u64) -> Option<Self> {
if id > 0 { Some(UserId(id)) } else { None }
}
}When to reach for this: Domain IDs, validated strings, or implementing traits on foreign types.
Working Example
struct Email(String);
impl Email {
pub fn parse(s: impl Into<String>) -> Result<Self, &'static str> {
let s = s.into();
if s.contains('@') { Ok(Email(s)) } else { Err("invalid email") }
}
}
impl AsRef<str> for Email {
fn as_ref(&self) -> &str { &self.0 }
}What this demonstrates:
UserIdandOrderIdare not interchangeable though both wrapu64- Validation centralized in constructor/
TryFrom repr(transparent)preserves ABI for FFI wrappers- Orphan rule bypass: impl local trait on
Email, not onStringdirectly
Deep Dive
Newtype vs Alias
type UserId = u64; // alias: same type
struct UserId(u64); // newtype: distinct typeserde
#[serde(transparent)]
struct Meters(f64);Gotchas
- Public inner field - bypasses validation. Fix: private field + constructor.
- Excessive Deref - newtype becomes alias ergonomically. Fix: impl only needed methods, avoid
Derefto inner unless intentional. - Forgot Eq/Hash for IDs - map keys break. Fix:
#[derive(PartialEq, Eq, Hash)]on newtype. - FFI without transparent repr - layout mismatch. Fix:
#[repr(transparent)]on single-field wrapper.
Alternatives
| Alternative | Use When | Don't Use When |
|---|---|---|
| Type alias | Internal clarity only | Need type safety at API |
| Enum | Small fixed set of states | Unbounded IDs |
| Phantom generic | Branding lifetimes | Simple ID wrap |
FAQs
Can I impl Display?
Yes on newtype; control formatting and redaction.
newtype for String?
Common for Email, Username, Slug with validation.
Performance cost?
Zero at runtime; same layout as inner with transparent repr.
sqlx mapping?
UserId(row.get::<i64,_>(0)? as u64) or FromRow custom impl.
clap args?
Parse into newtype in clap value_parser for validated CLI input.
newtype in HashMap key?
HashMap<UserId, User> prevents mixing key types.
Transparent and ZST?
Transparent requires single non-ZST field.
Document invariant?
Rustdoc on struct explains what values are legal.
TryFrom vs new?
TryFrom for conversions; inherent new for domain API.
newtype over Arc?
Possible for shared handle branding; less common than Copy IDs.
Related
- Rust Patterns Basics - overview
- From/Into & TryFrom - conversions
- Domain Modeling with Types - design
- Rust API Guidelines
Stack versions: This page was written for Rust 1.97.0 (edition 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4, and Polars 0.46+.