Advanced Types ​
Updated Mar 2026Type aliases, the never type (!), dynamically sized types, function pointers, closures as return types, and PhantomData for zero-cost type-level programming.
Type Aliases ​
Create a new name for an existing type:
type Kilometers = i32;
let distance: Kilometers = 5;
let speed: i32 = 10;
// These are the same type -- fully interchangeable
let time = distance / speed;Reducing Repetition ​
Type aliases shine when the original type is long:
// Before: verbose
fn takes_long_type(f: Box<dyn Fn() + Send + 'static>) { /* ... */ }
fn returns_long_type() -> Box<dyn Fn() + Send + 'static> { /* ... */ }
// After: clean
type Thunk = Box<dyn Fn() + Send + 'static>;
fn takes_thunk(f: Thunk) { /* ... */ }
fn returns_thunk() -> Thunk { /* ... */ }The std::io::Result Pattern ​
The standard library uses type aliases extensively:
// In std::io:
type Result<T> = std::result::Result<T, std::io::Error>;
// Now io functions use the short form:
pub fn read_to_string(&mut self, buf: &mut String) -> Result<usize> { /* ... */ }
// Instead of: -> std::result::Result<usize, std::io::Error>Type Alias vs Newtype ​
| Feature | Type Alias | Newtype |
|---|---|---|
| Creates new type | No (same type) | Yes (distinct type) |
| Type checking | Interchangeable | Prevents mixing |
| Method access | All methods available | Must impl Deref or wrap |
| Use case | Reducing verbosity | Type safety |
type Meters = f64;
type Seconds = f64;
let m: Meters = 5.0;
let s: Seconds = 3.0;
let _ = m + s; // compiles -- they're both f64
struct MetersNT(f64);
struct SecondsNT(f64);
// MetersNT(5.0) + SecondsNT(3.0) // won't compile -- different typesThe Never Type (!) ​
The ! type represents computations that never produce a value:
// Functions that never return
fn diverges() -> ! {
panic!("This function never returns");
}
fn infinite_loop() -> ! {
loop {
// runs forever
}
}Why ! Exists ​
! can be coerced into any type, which makes control flow expressions work:
// continue has type ! -- coerces to u32 in this context
let guess: u32 = match input.trim().parse() {
Ok(num) => num,
Err(_) => continue, // ! coerces to u32
};
// panic! has type ! -- coerces to T in unwrap
impl<T> Option<T> {
pub fn unwrap(self) -> T {
match self {
Some(val) => val,
None => panic!("called unwrap on None"), // ! coerces to T
}
}
}
// Infinite loops have type !
let value: i32 = loop {
break 42; // breaks with i32 value
};Expressions That Return ! ​
| Expression | Type | Why |
|---|---|---|
panic!(...) | ! | Terminates the thread |
loop { ... } (no break) | ! | Runs forever |
continue | ! | Jumps to loop start |
return | ! | Exits function |
std::process::exit(0) | ! | Terminates process |
unreachable!() | ! | UB hint to compiler |
Dynamically Sized Types (DSTs) ​
Most types have a known size at compile time. DSTs do not:
// str is a DST -- its length isn't known at compile time
// You can't use str directly:
// let s: str = "hello"; // ERROR: size not known
// Must use behind a pointer:
let s: &str = "hello"; // fat pointer: (ptr, length)
let s: Box<str> = "hello".into(); // heap-allocated DSTThe Golden Rule ​
DST values must always live behind some kind of pointer.
Common pointer types for DSTs:
| Pointer | Use Case |
|---|---|
&T | Borrowed reference |
&mut T | Mutable reference |
Box<T> | Owned heap allocation |
Rc<T> | Reference-counted |
Arc<T> | Atomically reference-counted |
Fat Pointers ​
References to DSTs are "fat pointers" -- twice the size of normal pointers:
use std::mem::size_of;
// Regular pointer: just an address
assert_eq!(size_of::<&u32>(), 8); // 8 bytes on 64-bit
// Fat pointer to str: address + length
assert_eq!(size_of::<&str>(), 16); // 16 bytes on 64-bit
// Fat pointer to dyn Trait: address + vtable pointer
assert_eq!(size_of::<&dyn ToString>(), 16); // 16 bytesThe Sized Trait ​
Every generic type parameter has an implicit Sized bound:
// These are equivalent:
fn generic<T>(t: T) { }
fn generic<T: Sized>(t: T) { }Use ?Sized to opt out and accept DSTs:
// T can be a DST, but must be behind a reference
fn generic<T: ?Sized>(t: &T) {
// t is a fat pointer if T is unsized
}
// Now works with both:
generic(&42_u32); // T = u32 (Sized)
generic("hello"); // T = str (not Sized, passed as &str)Common DSTs ​
| DST | Sized Equivalent | Fat Pointer |
|---|---|---|
str | String | &str (ptr + len) |
[T] | Vec<T> | &[T] (ptr + len) |
dyn Trait | Box<dyn Trait> | &dyn Trait (ptr + vtable) |
Path | PathBuf | &Path (ptr + len) |
OsStr | OsString | &OsStr (ptr + len) |
CStr | CString | &CStr (ptr + len) |
Function Pointers ​
The fn type (lowercase) represents a function pointer:
fn add_one(x: i32) -> i32 { x + 1 }
// fn is a type, not a trait
fn do_twice(f: fn(i32) -> i32, arg: i32) -> i32 {
f(arg) + f(arg)
}
let answer = do_twice(add_one, 5);
assert_eq!(answer, 12);fn vs Fn ​
| Feature | fn (type) | Fn (trait) |
|---|---|---|
| What is it | Concrete function pointer | Trait bound |
| Accepts closures | Only non-capturing | All closures |
| Size | Always one pointer | Varies (captures) |
| FFI compatible | Yes | No |
// fn: only accepts function pointers and non-capturing closures
fn takes_fn_pointer(f: fn(i32) -> i32) { }
takes_fn_pointer(add_one); // OK
takes_fn_pointer(|x| x + 1); // OK (non-capturing)
// Fn: accepts any callable
fn takes_fn_trait(f: impl Fn(i32) -> i32) { }
takes_fn_trait(add_one); // OK
takes_fn_trait(|x| x + 1); // OK
let offset = 5;
takes_fn_trait(|x| x + offset); // OK (capturing closure)Enum Variants as Function Pointers ​
Enum variant constructors are function pointers:
enum Status {
Value(u32),
Stop,
}
// Status::Value is fn(u32) -> Status
let list_of_statuses: Vec<Status> =
(0u32..20).map(Status::Value).collect();Closures as Return Types ​
Returning a Single Closure Type ​
fn returns_closure() -> impl Fn(i32) -> i32 {
|x| x + 1
}Returning Different Closures ​
Each closure has a unique anonymous type, so you can't return different closures with impl Fn:
// ERROR: different closures have different types
// fn returns_either(flag: bool) -> impl Fn(i32) -> i32 {
// if flag { |x| x + 1 } else { |x| x - 1 }
// }
// FIX: use trait objects
fn returns_either(flag: bool) -> Box<dyn Fn(i32) -> i32> {
if flag {
Box::new(|x| x + 1)
} else {
Box::new(|x| x - 1)
}
}PhantomData -- Zero-Cost Type Markers ​
PhantomData<T> is a zero-sized type that tells the compiler "this struct logically owns a T":
use std::marker::PhantomData;
// Without PhantomData: compiler doesn't know Iter borrows from 'a
struct Iter<'a, T> {
ptr: *const T,
end: *const T,
_marker: PhantomData<&'a T>, // "I logically hold &'a T"
}
// PhantomData affects:
// - Lifetime checking (struct borrows from 'a)
// - Drop checking (T might need dropping)
// - Variance (covariant in T for PhantomData<T>)
// - Auto traits (Send/Sync depend on T)Common PhantomData Patterns ​
// Mark ownership of T (affects drop check)
struct MyVec<T> {
ptr: *mut T,
len: usize,
cap: usize,
_marker: PhantomData<T>, // owns T values
}
// Mark lifetime relationship
struct Ref<'a, T: 'a> {
ptr: *const T,
_marker: PhantomData<&'a T>, // borrows T for 'a
}
// Type-level state machine (typestate pattern)
struct Locked;
struct Unlocked;
struct Door<State> {
_state: PhantomData<State>,
}
impl Door<Locked> {
fn unlock(self) -> Door<Unlocked> {
Door { _state: PhantomData }
}
}
impl Door<Unlocked> {
fn lock(self) -> Door<Locked> {
Door { _state: PhantomData }
}
fn open(&self) {
println!("Door is open");
}
}
let door = Door::<Locked> { _state: PhantomData };
// door.open(); // ERROR: no method open() on Door<Locked>
let door = door.unlock();
door.open(); // OK: Door<Unlocked> has open()Quick Reference ​
| Type Feature | Syntax | Purpose |
|---|---|---|
| Type alias | type Name = Existing; | Reduce verbosity |
| Never type | -> ! | Functions that don't return |
| DST bound | T: ?Sized | Accept dynamically sized types |
| Function pointer | fn(i32) -> i32 | FFI-compatible callable |
| Closure trait | impl Fn(i32) -> i32 | Any callable |
| Boxed closure | Box<dyn Fn(i32) -> i32> | Heterogeneous callables |
| PhantomData | PhantomData<T> | Zero-cost type marker |