Rust Patterns That Matter #1: Newtype
Post 1 of 22 in Rust Patterns That Matter. Companion series: Building a Chat Server in Rust.
Next: #2: From / Into Conversions
Two function parameters have the same type. A caller swaps them by accident. The
program compiles, runs, and produces wrong results. The type system could have caught
this, but both parameters were u64 and the compiler can't tell them
apart.
The motivation
fn transfer(from: u64, to: u64, amount: u64) {
// ...
}
// Oops: swapped from and to
transfer(recipient_id, sender_id, 500);
This compiles. All three arguments are u64. The compiler has no way
to know that from is a sender and to is a recipient.
You might try type aliases:
type UserId = u64;
type Amount = u64;
fn transfer(from: UserId, to: UserId, amount: Amount) { /*...*/ }
But type aliases are transparent. UserId and u64 are
the same type to the compiler. You can still pass an Amount where a
UserId is expected. No protection.
The pattern: newtype
Wrap the inner type in a single-field tuple struct:
struct UserId(u64);
struct OrderId(u64);
struct Amount(u64);
fn transfer(from: UserId, to: UserId, amount: Amount) {
// ...
}
fn main() {
let sender = UserId(42);
let recipient = UserId(99);
let amount = Amount(500);
transfer(sender, recipient, amount); // OK
let order = OrderId(7);
// transfer(order, recipient, amount);
// ERROR: expected `UserId`, found `OrderId`
}
UserId, OrderId, and Amount are now
distinct types. Passing an OrderId where a UserId is
expected is a compile error. The type system catches the bug.
Zero-cost abstraction
A newtype compiles to exactly the same machine code as the inner type. There's no
wrapper struct at runtime, no indirection, no overhead. UserId(42)
is just 42 in memory. The distinction exists only in the type system,
and the type system is erased at compile time.
The orphan rule workaround
Rust's orphan rule prevents you from implementing a foreign trait on a foreign type.
You can't write impl Display for Vec<u8> in your crate -
both Display and Vec are defined elsewhere.
The newtype pattern gives you a local type to attach the impl to:
use std::fmt;
struct Bytes(Vec<u8>);
impl fmt::Display for Bytes {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{} bytes", self.0.len())
}
}
Bytes is a type in your crate, so you can implement any trait on it.
Inside, it's just a Vec<u8>.
Accessing the inner value
The simplest approach: access the field directly with .0:
let id = UserId(42);
let raw: u64 = id.0;
For a more explicit API, add a method:
impl UserId {
fn as_u64(&self) -> u64 { self.0 }
}
You can also implement Deref<Target = u64> to make the newtype
transparent for reads. But be deliberate - Deref makes the
compiler automatically coerce &UserId to &u64,
which weakens the type safety you added. If you want UserId and
u64 to be interchangeable for reads, implement Deref.
If you want the distinction enforced everywhere, don't.
Deriving traits
Newtypes often need the same traits as their inner type. Use derive:
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
struct UserId(u64);
This gives you hashing (for HashMap keys), equality, debug printing,
and cheap copies - everything you'd expect from a u64 wrapper.
When to use it
- Type safety for IDs:
UserIdvsOrderIdvsProductId - Units of measurement:
Meters(f64)vsSeconds(f64) - Orphan rule workaround: implementing foreign traits on foreign types
- Restricting APIs: a
NonEmpty(Vec<T>)that enforces the vector is never empty at construction time - Type-safe indices:
NodeId(usize)for arenas (#8)
When not to: if you genuinely want the types to be interchangeable (a
type alias is fine), or if the wrapping adds ceremony without catching
real bugs.
See it in practice: Building a Chat Server #1: Hello, TCP
uses this pattern for type-safe UserId and RoomId.
Telex