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.
// 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.
Object-safety rules
Section titled “Object-safety rules”Not every trait can be made into a trait object. A trait is object-safe 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 object-safe because clone() returns Self. Display and Debug are object-safe.
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…