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.

// 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 object-safe 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 object-safe because clone() returns Self. Display and Debug are object-safe.

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 object-safe?