Skip to content

Trait Objects

TypeScript interfaces vs Rust trait objects

Section titled “TypeScript interfaces vs Rust trait objects”

TypeScript interfaces enable runtime polymorphism with zero ceremony — you pass any object that satisfies the interface’s shape. Rust’s equivalent is a trait object: a value preceded by dyn, which tells the compiler to use a vtable for dynamic dispatch instead of monomorphising.

You have two choices in Rust:

ApproachSyntaxDispatchWhen to use
Generic with boundfn foo<T: Animal>(a: T)Static (monomorphised)When all types are known at compile time
Trait objectfn foo(a: &dyn Animal)Dynamic (vtable)When the concrete type is unknown at compile time
TypeScript
// TypeScript — interface polymorphism
interface Animal {
speak(): string;
name(): string;
}
class Dog implements Animal {
speak() { return "Woof"; }
name() { return "Rex"; }
}
class Cat implements Animal {
speak() { return "Meow"; }
name() { return "Whiskers"; }
}
function makeNoise(animal: Animal): void {
console.log(`${animal.name()} says ${animal.speak()}`);
}
const animals: Animal[] = [new Dog(), new Cat()];
animals.forEach(makeNoise);
Rust
trait Animal {
fn speak(&self) -> &str;
fn name(&self) -> &str;
}
struct Dog { name: String }
struct Cat { name: String }
impl Animal for Dog {
fn speak(&self) -> &str { "Woof" }
fn name(&self) -> &str { &self.name }
}
impl Animal for Cat {
fn speak(&self) -> &str { "Meow" }
fn name(&self) -> &str { &self.name }
}
// &dyn Animal = reference to any type implementing Animal
fn make_noise(animal: &dyn Animal) {
println!("{} says {}", animal.name(), animal.speak());
}
fn main() {
// Vec of boxed trait objects — types can differ at runtime
let animals: Vec<Box<dyn Animal>> = vec![
Box::new(Dog { name: String::from("Rex") }),
Box::new(Cat { name: String::from("Whiskers") }),
];
for a in &animals {
make_noise(a.as_ref());
}
}

Box<dyn Trait> — heap-allocated trait objects

Section titled “Box<dyn Trait> — heap-allocated trait objects”

When you need to store trait objects in a collection (like a Vec) or return them from a function, you must box them. Box<dyn Trait> is a fat pointer: it holds the address of the data on the heap and a pointer to the vtable.

// @skip-verify fragment: builds on the Animal/Dog/Cat example above (dynRs), not standalone
// Returning a trait object from a function
fn new_animal(kind: &str) -> Box<dyn Animal> {
match kind {
"dog" => Box::new(Dog { name: String::from("Rex") }),
_ => Box::new(Cat { name: String::from("Whiskers") }),
}
}

This is the Rust equivalent of returning an interface type from a TypeScript factory function.

Not every trait can be made into a trait object. A trait is dyn compatible if:

  1. It has no generic methods (generics would require monomorphisation, breaking the vtable).
  2. Its methods do not return Self by value.

The standard library’s Clone trait is not dyn compatible because clone() returns Self. Display and Debug are dyn compatible.

Older Rust material calls this object safety / object-safe — the compiler’s own diagnostic (rustc 1.96) now reads error[E0038]: the trait '...' is not dyn compatible.

trait Shape {
fn area(&self) -> f64;
fn name(&self) -> &str;
}
struct Circle { radius: f64 }
struct Rectangle { width: f64, height: f64 }
impl Shape for Circle {
fn area(&self) -> f64 { std::f64::consts::PI * self.radius * self.radius }
fn name(&self) -> &str { "Circle" }
}
impl Shape for Rectangle {
fn area(&self) -> f64 { self.width * self.height }
fn name(&self) -> &str { "Rectangle" }
}
fn print_area(shape: &dyn Shape) {
println!("{}: area = {:.2}", shape.name(), shape.area());
}
fn main() {
let shapes: Vec<Box<dyn Shape>> = vec![
Box::new(Circle { radius: 3.0 }),
Box::new(Rectangle { width: 4.0, height: 5.0 }),
Box::new(Circle { radius: 1.5 }),
];
for s in &shapes {
print_area(s.as_ref());
}
let total: f64 = shapes.iter().map(|s| s.area()).sum();
println!("Total area: {:.2}", total);
}
What is a trait object in Rust?
Why must trait objects usually be stored as `Box<dyn Trait>` rather than `dyn Trait` directly?
When should you prefer `&dyn Trait` over `<T: Trait>`?
Which of the following standard-library traits is NOT dyn compatible?