Dynamic Typing

Determine the Type of an Object at Runtime and Downcast with the Any Trait

std

Dynamic typing in Rust is limited compared to other languages, but the std::any↗ module provides ways to obtain type information at runtime.

std::any is primarily used in scenarios where you need to store and retrieve data of various, unknown concrete types within a collection or context that uses trait objects (specifically dyn Any). This often comes up in:

  • Plugins: If you're building a system where users can register custom data or components, and you need to store them polymorphically, Any↗ allows you to later retrieve them and process them based on their original concrete type.
  • Generic Event Systems: In an event system where different types of events might be processed by a single handler, Any can help identify and process specific event types.
  • Reflection-like capabilities: While Rust doesn't have full reflection like some other languages, Any provides a limited form of runtime type inspection.

The TypeId↗ struct represents a globally unique, opaque identifier for a type, allowing you to compare types without needing to know their names at compile time. A TypeId is currently only available for types which are 'static.

You can use TypeId to check if two types are the same or as a key to store type information in e.g. a HashMap:

use std::any::Any;
use std::any::TypeId;

// `of` returns the `TypeId` of the generic type parameter.
//
// The `'static` bound below is necessary,
// because `TypeId` only works with `'static` types.
fn is_same_type<T: 'static, U: 'static>() -> bool {
    TypeId::of::<T>() == TypeId::of::<U>()
}

fn main() {
    // Compare types with `TypeId::of`:
    println!("i32 vs i32: {}", is_same_type::<i32, i32>()); // true.
    println!("i32 vs u32: {}", is_same_type::<i32, u32>()); // false.

    // The `Any` trait can be used to get a `TypeId`:
    let value: &dyn Any = &"hello";
    let tid = value.type_id();
    // `TypeId` implements `Debug` but its value is just an opaque number.
    // See below to get a type name.
    println!("TypeId of value: {tid:?}");

    // However, given a smart pointer containing `dyn Any`,
    // calling `.type_id()` on the smart pointer will produce the `TypeId` of
    // the container, not the underlying object:
    let boxed: Box<dyn Any> = Box::new(3_i32);
    #[allow(clippy::type_id_on_box)]
    let boxed_id = boxed.type_id();
    // To get the `TypeId` of the inner value,
    // use `*` to first dereference the `Box` (calling `Deref`).
    let actual_id = (*boxed).type_id();

    assert_eq!(actual_id, TypeId::of::<i32>());
    assert_eq!(boxed_id, TypeId::of::<Box<dyn Any>>());

    // To get a type _name_ for diagnostics purposes, use `std::any::type_name`
    // or `type_name_of_val`. Note: the returned name may vary with compiler
    // versions.
    println!("{}", std::any::type_name::<String>());
}

The std::any::Any↗ trait is the core of the std::any module. It is a special built-in trait that determines the concrete type of a trait object at runtime and provides the ability to downcast a trait object to a concrete type, when the concrete type is known ("downcasting" converts a reference of a general type (here dyn Any and related types) to a reference of a more specific type, if the value is of that type). The Any trait provides a few methods:

use std::any::Any;

// - `is::<T>(&self) -> bool` checks if the underlying concrete type of the
//   trait object is `T`.
// - `downcast_ref::<T>(&self) -> Option<&T>` attempts to downcast the trait
//   object to a reference `&T`.
// It returns `Some(&T)` if successful, `None` otherwise.
// - `downcast_mut::<T>(&mut self) -> Option<&mut T>` is similar to
//   `downcast_ref`, but provides a mutable reference.
//
// All are implemented on `dyn Any`, `dyn Any + Send`, `dyn Any + Send + Sync`.
fn is_string(s: &dyn Any) -> bool {
    // `is<T>` is equivalent to `TypeId::of::<T>() == s.type_id()`.
    let res = s.is::<String>();
    if res {
        print!("String: ");
    } else {
        println!("Not a string...");
    }
    // Get access to the `&String` if `s` is of the right type:
    if let Some(string) = s.downcast_ref::<String>() {
        println!("{string}");
    }
    res
}

// `downcast::<T>(self: Box<Self>) -> Result<Box<T>, Box<Self>>` consumes a
// `Box<dyn Any>` and attempts to downcast it to a `Box<T>`. `downcast` is
// implemented on `Box<dyn Any>`, `Box<dyn Any + Send>`, `Box<dyn Any + Send +
// Sync>`.
fn print_if_string(value: Box<dyn Any>) {
    if let Ok(string) = value.downcast::<String>() {
        println!("String: {string}");
    }
}

fn main() {
    assert!(!is_string(&42));
    let s = "a string".to_string();
    assert!(is_string(&s));

    print_if_string(Box::new(s));
}

Note a few caveats with Any:

  • The Any trait can only be used with types that have a 'static lifetime. This means they must not contain any non-'static references.
  • Runtime Overhead: Downcasting involves runtime checks (is::<T>↗). While not excessively slow, it's less performant than static dispatch.
  • Loss of Compile-Time Guarantees: When you put something into Box<dyn Any> or similar, you lose type information at compile time. This makes it harder for the compiler to catch errors for you.
  • If there's a way to achieve your goal using generics with trait bounds, enums, or other more type-safe Rust patterns, those are generally preferred over Any. Any should be used when you genuinely need runtime type introspection for heterogeneous collections.

Compile-time Reflection

reflect reflect~crates.io reflect~repo reflect~lib.rs cat~development-tools::procedural-macro-helpers

The "but I thought Rust doesn't have reflection?" memorial brand new way of defining procedural macros.

  • Development Tools: Cargo Plugins.
  • Scripting.
  • Traits.