🚗 Phantom Types in Rust
Sometimes in Rust, we want to tag a type with extra information without actually storing that information at runtime. That’s where phantom types and PhantomData
come in.

What is PhantomData?
PhantomData<T>
is a special zero-sized type in Rust that tells the compiler “I want to use this type T
in my struct, but I don’t actually need to store any data of that type.” It’s called “phantom” because the type exists only at compile time – it disappears completely when your program runs.
The Problem PhantomData Solves
Imagine you want to create a generic struct but don’t actually need to store data of the generic type:
1. The Setup:

2. The Rust Code:
use std::marker::PhantomData;
struct Vehicle<Engine> {
model: String,
engine: PhantomData<Engine>,
}
When we construct a Vehicle, we still need to provide the phantom data, although it will be optimized out at compile time:
use std::marker::PhantomData;
let my_tesla: Vehicle<Electric> = Vehicle {
model: "Model S".into(),
engine: PhantomData,
};
We can return the name of the engine type without storing that value as state or as a separate field within the structure:
impl Vehicle<Electric> {
fn engine_type(&self) -> &str {
"electric"
}
}
impl Vehicle<Gasoline> {
fn engine_type(&self) -> &str {
"gasoline"
}
}
impl Vehicle<Hybrid> {
fn engine_type(&self) -> &str {
"hybrid"
}
}
impl Vehicle<Diesel> {
fn engine_type(&self) -> &str {
"diesel"
}
}
Testing Our Code
Finally, we can test our code as follows:
let my_tesla: Vehicle<Electric> = Vehicle {
model: "Model S".into(),
engine: PhantomData,
};
println!(
"My vehicle is a {} with a {} engine",
my_tesla.model,
my_tesla.engine_type(),
);
My vehicle is a Model S with a electric engine
“Without Runtime Overhead”
The beauty is that PhantomData<T>
has zero size – it adds no memory cost to your struct. The type information exists only during compilation to ensure correctness, then vanishes completely in the final executable.
This gives you the safety of strong typing with the performance of having no extra data stored at runtime! 👍
Full code :
use std::marker::PhantomData;
// Marker types for different engine kinds
struct Electric;
struct Gasoline;
struct Hybrid;
struct Diesel;
// Generic vehicle type with phantom data for engine
struct Vehicle<Engine> {
model: String,
engine: PhantomData<Engine>,
}
// Implementations for each engine type
impl Vehicle<Electric> {
fn engine_type(&self) -> &str {
"electric"
}
}
impl Vehicle<Gasoline> {
fn engine_type(&self) -> &str {
"gasoline"
}
}
impl Vehicle<Hybrid> {
fn engine_type(&self) -> &str {
"hybrid"
}
}
impl Vehicle<Diesel> {
fn engine_type(&self) -> &str {
"diesel"
}
}
fn main() {
// Example vehicles
let my_tesla: Vehicle<Electric> = Vehicle {
model: "Model S".into(),
engine: PhantomData,
};
let my_camry: Vehicle<Gasoline> = Vehicle {
model: "Toyota Camry".into(),
engine: PhantomData,
};
let my_prius: Vehicle<Hybrid> = Vehicle {
model: "Toyota Prius".into(),
engine: PhantomData,
};
let my_truck: Vehicle<Diesel> = Vehicle {
model: "Ford F-250".into(),
engine: PhantomData,
};
// Print details
println!(
"My vehicle is a {} with a {} engine",
my_tesla.model,
my_tesla.engine_type()
);
println!(
"My vehicle is a {} with a {} engine",
my_camry.model,
my_camry.engine_type()
);
println!(
"My vehicle is a {} with a {} engine",
my_prius.model,
my_prius.engine_type()
);
println!(
"My vehicle is a {} with a {} engine",
my_truck.model,
my_truck.engine_type()
);
}
👉 Why use phantom marker types (Vehicle<Electric>
) instead of a trait like Engine
with fn engine_type(&self) -> &str
?
Here’s the breakdown:
✅ When to use phantom types / marker types (your example)
- Compile-time distinction:
Vehicle<Electric>
andVehicle<Diesel>
are different concrete types. The compiler knows them apart and won’t let you mix them.fn refuel(vehicle: Vehicle<Gasoline>) { /* ... */ } let tesla = Vehicle::<Electric> { /*...*/ }; refuel(tesla); // ❌ compile error: wrong type
- Zero runtime cost:
ThePhantomData
is optimized away. No vtables, no dynamic dispatch, no extra data. The “engine type” is purely a compile-time tag. - Sealed set of options:
You control which markers exist (Electric
,Diesel
, etc.). Nobody outside your module can add their own unless you expose the markers. - Type-driven guarantees:
You can encode rules at the type level:struct ChargingStation; fn charge(_: Vehicle<Electric>) {} fn charge(_: Vehicle<Gasoline>) {} // ❌ won't even compile
This approach is more type-safe and better when you want the compiler to enforce rules about valid operations.
✅ When to use traits
- Polymorphism:
You can write functions that work with any engine type implementing the trait:trait Engine { fn engine_type(&self) -> &str; } struct Electric; impl Engine for Electric { fn engine_type(&self) -> &str { "electric" } } struct Vehicle<E: Engine> { model: String, engine: E, } fn print_engine<E: Engine>(v: &Vehicle<E>) { println!("{} with {}", v.model, v.engine.engine_type()); }
- Extensible by others:
Anyone can implement yourEngine
trait for their own type. That’s useful for libraries. - Behavior-driven:
Traits are about what you can do with a type, not what kind of thing it is.
⚖️ Rule of Thumb
- Use phantom types when you want strong, closed-world type distinctions at compile time.
- Use traits when you want extensibility and shared behavior across potentially many unknown types.
👉 So your Vehicle<Electric>
style is great if you want the compiler to forbid nonsense at the type level (e.g. charging a diesel).
A trait-based Engine
would be better if you want to let users of your code define new engine types or treat all engines polymorphically.
🔹 Alternative :
Using Phantom types + generics
- Compile-time distinction:
AVehicle<Diesel>
is a different type from aVehicle<Petrol>
.
That means the compiler enforces engine-specific methods. - Pros: zero-cost, no runtime tag, very strong type-safety.
- Cons: you can’t pick the engine at runtime — it’s “baked into the type”.
enum Engine {
Diesel,
Petrol,
Electric,
}
struct Vehicle {
model: String,
engine: Engine,
}
impl Vehicle {
fn engine_type(&self) -> &str {
match self.engine {
Engine::Diesel => "Diesel Engine",
Engine::Petrol => "Petrol Engine",
Engine::Electric => "Electric",
}
}
}
fn main() {
let diesel_car = Vehicle { model: "Astra".to_string(), engine: Engine::Diesel };
println!("{}", diesel_car.engine_type());
}
🚫 What phantom types don’t do
- They don’t “check” whether
"Model S"
is really an electric car. - They don’t validate runtime data.
- They only enforce compile-time type-level rules.