Type System Fundamentals

Phantom Types

A phantom type is a generic type or lifetime parameter on a struct or enum that does not appear in any runtime data field, bound instead using the zero-sized marker std::marker::PhantomData<T>.

Why Unreferenced Type Parameters Are Rejected (E0392)

If you declare a struct struct TaggedId<Tag> { raw: u64 } without referencing Tag in any field, the compiler rejects the definition with E0392 because it cannot determine the variance, drop behavior, or auto-trait (Send/Sync) properties of Tag:

// ❌ does not compile
struct TaggedId<Tag> {
    raw: u64,
}

fn main() {}
error[E0392]: type parameter `Tag` is never used
 --> src/main.rs:2:17
  |
2 | struct TaggedId<Tag> {
  |                 ^^^ unused type parameter
  |
  = help: consider removing `Tag`, referring to it in a field, or using a marker such as `PhantomData`

Adding a field _marker: std::marker::PhantomData<Tag> binds the parameter at the type level while occupying 0 bytes of memory at runtime (size_of::<PhantomData<T>>() == 0).

Type-Safe Tagged Identifiers and Units of Measure

Using PhantomData<Tag>, a single generic struct Id<Entity> or Amount<Currency> can represent dozens of incompatible domain types without duplicating code or paying any runtime overhead:

use std::marker::PhantomData;

#[derive(Debug, PartialEq, Eq)]
pub struct User;

#[derive(Debug, PartialEq, Eq)]
pub struct Invoice;

#[derive(Debug, PartialEq, Eq)]
pub struct Id<Entity> {
    value: u64,
    _marker: PhantomData<Entity>,
}

impl<Entity> Id<Entity> {
    pub const fn new(value: u64) -> Self {
        Self {
            value,
            _marker: PhantomData,
        }
    }

    pub const fn get(&self) -> u64 {
        self.value
    }
}

fn lookup_invoice(id: Id<Invoice>) {
    println!("Looking up invoice #{}, size = {} bytes", id.get(), size_of_val(&id));
}

fn main() {
    let _user_id = Id::<User>::new(42);
    let invoice_id = Id::<Invoice>::new(42);

    lookup_invoice(invoice_id);
    // `lookup_invoice(_user_id)` is a compile-time error: expected `Id<Invoice>`, found `Id<User>`!
}
Looking up invoice #42, size = 8 bytes

Controlling Variance, Auto Traits, and Lifetimes with PhantomData

Beyond phantom type tags, PhantomData tells the compiler how a struct containing raw pointers or FFI handles behaves with respect to lifetimes, variance, and Send / Sync:

PhantomData Field Type Meaning Communicated to the Compiler Variance in T / 'a Send / Sync Impact
PhantomData<T> Struct logically owns a T (may affect drop check) Covariant Send/Sync iff T: Send/Sync
PhantomData<&'a T> Struct logically borrows a shared &'a T (e.g., raw pointer slice wrapper) Covariant in 'a and T Send/Sync iff &'a T: Send/Sync
PhantomData<&'a mut T> Struct logically holds an exclusive mutable borrow &'a mut T Covariant in 'a, Invariant in T Send/Sync iff &'a mut T: Send/Sync
PhantomData<fn() -> T> Covariant marker without claiming ownership of T Covariant Always Send + Sync (even if T: !Send)
PhantomData<fn(T) -> T> Invariant marker without claiming ownership of T Invariant Always Send + Sync
PhantomData<*const ()> Opts a struct out of Send and Sync — !Send and !Sync