Home

Downcast Trait Object

Evergreen 🌳 - Published July 30, 2020 - (Updated October 5, 2022)

Downcasting is Rust's method of converting a trait into a concrete type.

It is done using the Any trait, which allows "dynamic typing of any 'static type through runtime reflection" (docs).

Here's an example showing a simple case of having a trait object that you want to change back into it's original type:

trait Print {
  fn start_printing(&self);
}

struct TrustyPrinter;

impl Print for TrustyPrinter {
  fn start_printing(&self) {
    println!("Starting!");
  }
}

fn main() {
  // Create your struct that implements your trait
  let t = TrustyPrinter;

  // Use the trait to abstract away everything that is not needed
  let x: &dyn Print = &t;

  // Now there's an edge case that uses the original type..
  // How do you change it back?
  let printer: &TrustyPrinter = x;
}

In order to make use of it, you need to add a method into your trait so the

use std::any::Any;

trait Print {
  fn start_printing(&self);
  fn as_any(&self) -> &dyn Any;
}

struct TrustyPrinter;

impl Print for TrustyPrinter {
  fn start_printing(&self) {
    println!("Starting!");
  }
  fn as_any(&self) -> &dyn Any {
    self
  }
}

You can now use the downcast_ref method to convert the reference to your trait object back into your concrete type.

fn main() {
  // Create your struct that implements your trait
  let t = TrustyPrinter;

  // Use the trait to abstract away everything that is not needed
  let x: &dyn Print = &t;

  // Now you can use the `downcast_ref` method to convert it back
  // into `TrustyPrinter`
  let printer: &TrustyPrinter =
        x.as_any()
         .downcast_ref::<TrustyPrinter>()
         .expect("Wasn't a trusty printer!");

   printer.start_printing();
}

(Playground)

Voilà! You can now freely and carelessly convert between levels of abstraction.

Crates

There's a few crates for making this process easier. Namely mopa and downcast-rs.

Unfortunately, much like using Any, these crates only enable downcasting of traits that you own, meaning you couldn't downcast from traits from the standard library.

Important Notes

There's a reason why this is so tedious to do and why there's not many usages of this trait in the standard library - it's almost always a bad idea.

Traits exist to create a boundary between the different levels of abstraction, they're not supposed to be able to see outside their own little world. This goes against the concept of separation of concerns and can make your code difficult to understand.

Alternatives to this approach are discussed in my blog post Don't use boxed trait objects for struct internals.


Bennett is a Software Engineer working at CipherStash. He spends most of his day playing with TypeScript and his nights programming in Rust. You can follow him on Github or Twitter.
This work by Bennett Hardwick is licensed under CC BY-NC-SA 4.0Creative Commons CC logoCreative Commons BY logoCreative Commons NC logoCreative Commons SA logo.