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.