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.
Search across all documentation pages
Wrap an existing type in a thin struct to create a distinct type with its own invariants, trait implementations, and API without runtime overhead.
#[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.
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:
UserId and OrderId are not interchangeable though both wrap u64TryFromrepr(transparent) preserves ABI for FFI wrappersEmail, not on String directlytype UserId = u64; // alias: same type
struct UserId(u64); // newtype: distinct type#[serde(transparent)]
struct Meters(f64);Deref to inner unless intentional.#[derive(PartialEq, Eq, Hash)] on newtype.#[repr(transparent)] on single-field wrapper.| 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 |
Yes on newtype; control formatting and redaction.
Common for Email, Username, Slug with validation.
Zero at runtime; same layout as inner with transparent repr.
UserId(row.get::<i64,_>(0)? as u64) or FromRow custom impl.
Parse into newtype in clap value_parser for validated CLI input.
HashMap<UserId, User> prevents mixing key types.
Transparent requires single non-ZST field.
Rustdoc on struct explains what values are legal.
TryFrom for conversions; inherent new for domain API.
Possible for shared handle branding; less common than Copy IDs.
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+.
Reviewed by Chris St. John·Last updated Jul 16, 2026