Dynamic Typing

Determine the Type of an Object at Runtime and Downcast it 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.
  • Development Tools: Cargo Plugins.
  • Scripting.
  • Traits.