Advanced Type Features
Type Aliases and Newtypes
Type aliases (type Kilometers = f64;) and newtypes (struct Kilometers(f64);) both introduce a domain-specific name for an underlying representation, but they serve opposite roles in the type system: a type alias creates a transparent synonym that unifies with the underlying type, whereas a newtype creates a distinct nominal type that enforces static separation, encapsulation, and independent trait implementations.
Comprehensive Comparison
| Feature | Type Alias (type Alias = Inner;) |
Newtype (struct Wrapper(Inner);) |
|---|---|---|
| Type identity | Same type as Inner (Alias == Inner) |
Distinct nominal type (Wrapper != Inner) |
Prevents mixing with Inner? |
No (can pass Inner where Alias is expected) |
Yes (E0308: mismatched types) |
| Can enforce constructor invariants? | No (caller can construct any Inner directly) |
Yes (make .0 private and validate in Wrapper::new) |
| Can implement foreign traits (orphan rule)? | No (impl Display for Alias is impl Display for Inner $\rightarrow$ E0117) |
Yes (Wrapper is a local type!) |
Inherits all methods & traits of Inner? |
Yes (100% identical) | No (must #[derive(...)] or implement only desired traits) |
| Runtime memory overhead | 0 bytes |
0 bytes (especially with #[repr(transparent)]) |
Type Alias (`type UserId = u64;`):
fn delete_user(user: UserId, org: OrgId)
delete_user(org_id, user_id); // <-- Compiles silently! Both are just `u64`!
Newtype (`struct UserId(u64); struct OrgId(u64);`):
fn delete_user(user: UserId, org: OrgId)
delete_user(org_id, user_id); // <-- Compile Error E0308! Swapped IDs caught immediately!
Demonstrating Type Alias Compatibility vs. Newtype Safety
use std::fmt;
// 1. Type alias: convenient shorthand, but does NOT prevent mixing with raw `u64`:
type MillisAlias = u64;
// 2. Newtype: distinct nominal type with `#[repr(transparent)]` zero-cost guarantee:
#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct UserId(u64);
impl UserId {
pub const fn new(raw: u64) -> Option<Self> {
if raw > 0 { Some(Self(raw)) } else { None }
}
pub const fn get(self) -> u64 {
self.0
}
}
// Because `UserId` is a local newtype, we can implement foreign traits like `Display`:
impl fmt::Display for UserId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "usr_{:06}", self.0)
}
}
fn main() {
let raw_u64: u64 = 100;
let alias_val: MillisAlias = raw_u64 + 50; // Mixes freely with `u64`
println!("MillisAlias: {}", alias_val);
let user = UserId::new(42).unwrap();
println!("UserId: {} (raw = {}, size = {} B)", user, user.get(), size_of::<UserId>());
}
MillisAlias: 150
UserId: usr_000042 (raw = 42, size = 8 B)
Selective Trait Forwarding on Newtypes
Unlike Deref<Target = Inner> (which exposes all immutable &Inner methods on the newtype—sometimes allowing callers to bypass validation or semantic boundaries), idiomatic newtypes implement From<Newtype> for Inner, TryFrom<Inner> for Newtype, or AsRef<Inner> to keep conversions explicit and intentional.