Newtype Pattern and From/Into Conversions
A "newtype" is a single-field tuple struct that wraps another type: struct UserId(u64). It compiles down to exactly the underlying type, no runtime cost, but it gives the compiler a new, distinct type to type-check against. Combined with From/Into, this is the idiomatic way to make illegal states unrepresentable and to build APIs that convert between types automatically instead of forcing callers through ad-hoc constructor methods.
Type Safety: Making Mix-Ups a Compile Error
Two u64 parameters with the same underlying type are indistinguishable to the compiler, swap their argument order and nothing catches it.
fn transfer_ownership(user_id: u64, product_id: u64) { /* ... */ }
transfer_ownership(product_id, user_id); // compiles, silently wrong
Wrapping each in a newtype turns that mix-up into a type error instead of a runtime bug:
struct UserId(u64);
struct ProductId(u64);
fn transfer_ownership(user_id: UserId, product_id: ProductId) { /* ... */ }
transfer_ownership(product_id, user_id);
// ERROR: expected `UserId`, found `ProductId`
This costs nothing at runtime, UserId has the exact same memory layout as the u64 it wraps, but it moves an entire category of bug (passing the right type of value in the wrong position) from "hopefully caught in review or a test" to "doesn't compile."
The Orphan Rule: Implementing a Foreign Trait on a Foreign Type
Rust's orphan rule says you can only implement a trait for a type if you own the trait, the type, or both. You can't impl serde::Serialize for std::time::Duration from your own crate, you own neither. Wrapping the foreign type in a newtype gives you a type you do own, which you can implement the foreign trait for:
use std::time::Duration;
struct HumanDuration(Duration);
impl std::fmt::Display for HumanDuration {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}s", self.0.as_secs())
}
}
Neither Display nor Duration belong to your crate, but HumanDuration does, so the orphan rule is satisfied. This is the standard workaround any time you want a foreign type to behave with a foreign trait it doesn't already implement.
If you specifically want the wrapper to transparently expose the inner type's own methods (so callers can call Duration methods directly through a HumanDuration), implement Deref:
impl std::ops::Deref for HumanDuration {
type Target = Duration;
fn deref(&self) -> &Duration { &self.0 }
}
Reach for Deref sparingly, it's meant for "is essentially the underlying type, just with extra capabilities" wrappers. If the newtype exists specifically to restrict what you can do with the inner value (like UserId above, where you don't want arbitrary u64 arithmetic available), skip Deref and expose only the specific methods you intend callers to use.
From/Into: The Idiomatic Conversion
Rather than a one-off UserId::from_u64(n) or UserId::new(n) constructor, implement From:
struct UserId(u64);
impl From<u64> for UserId {
fn from(id: u64) -> Self {
UserId(id)
}
}
let id: UserId = 42.into(); // via the blanket Into impl
let id = UserId::from(42); // equivalent, explicit form
Implementing From<A> for B gets you Into<B> for A automatically, the standard library provides a blanket implementation, you never write impl Into by hand. This matters because it's the same mechanism powering the ? operator's automatic error conversion, from Error Handling in Practice: when ? returns early on an Err, it calls From::from to convert the error type at the call site into the function's declared error type. Implementing From<MyError> for AppError is what makes some_call_returning_my_error()? work inside a function that returns Result<T, AppError>.
TryFrom/TryInto: When the Conversion Can Fail
From is for conversions that always succeed. When a conversion might fail, narrowing a u64 into a u32, parsing a string into a number, use TryFrom instead, which returns a Result.
use std::convert::TryFrom;
let big: u64 = 5_000_000_000;
let narrowed: u32 = big as u32; // silently truncates, no warning
let narrowed: Result<u32, _> = u32::try_from(big); // Err, caught explicitly
as casts between integer types silently truncate on overflow, the kind of bug that only shows up once real data exceeds whatever range you tested with. TryFrom forces you to handle the failure case, the conversion either succeeds or you get an explicit Err to deal with.
struct Percentage(u8);
impl TryFrom<i32> for Percentage {
type Error = String;
fn try_from(value: i32) -> Result<Self, Self::Error> {
if (0..=100).contains(&value) {
Ok(Percentage(value as u8))
} else {
Err(format!("{value} is not a valid percentage"))
}
}
}
This is the same pattern as From, but for the (very common) case where "convert this" and "validate this" are the same operation, a Percentage that exists at all is guaranteed to hold a valid value, because the only way to construct one goes through try_from.
Accepting Flexible Input with impl Into<T>
A function that needs an owned String but forces every caller to write .to_string() on a &str literal is needlessly rigid. Accepting impl Into<String> instead lets callers pass either, the conversion happens once, inside the function:
struct User {
name: String,
}
impl User {
fn new(name: impl Into<String>) -> Self {
User { name: name.into() }
}
}
let a = User::new("Ferris"); // &str, converted internally
let b = User::new(String::from("Ferris")); // String, Into<String> is a no-op
This is a small thing, but it shows up constantly in well-designed APIs: parameter types declared as impl Into<T> rather than T directly, so callers aren't stuck writing conversions the function could've done itself in one line.
Key Takeaways
- A newtype (
struct UserId(u64)) costs nothing at runtime but gives the compiler a distinct type, turning "passed the right type of value in the wrong position" into a compile error. - The orphan rule blocks implementing a foreign trait on a foreign type directly; wrapping the type in a newtype you own is the standard workaround.
- Implement
From<A> for Binstead of a one-off constructor, it gets youInto<B>for free and is the same mechanism that powers?'s automatic error conversion. - Use
TryFrom/TryIntofor conversions that can fail (especially narrowing integer casts), instead ofas, which truncates silently with no error path. - Accept
impl Into<T>in function signatures that need an ownedT, so callers can pass either the owned type or something cheaply convertible into it without doing the conversion themselves.