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:
| Approach | Syntax | Dispatch | When to use |
|---|---|---|---|
| Generic with bound | fn foo<T: Animal>(a: T) | Static (monomorphised) | When all types are known at compile time |
| Trait object | fn foo(a: &dyn Animal) | Dynamic (vtable) | When the concrete type is unknown at compile time |
// TypeScript — interface polymorphisminterface 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);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 Animalfn 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 functionfn 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.
Dyn-compatibility rules
Section titled “Dyn-compatibility rules”Not every trait can be made into a trait object. A trait is dyn compatible if:
- It has no generic methods (generics would require monomorphisation, breaking the vtable).
- Its methods do not return
Selfby 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.
Try it
Section titled “Try it”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);}Compiling…