Any & Downcasting
|
This content was generated with the assistance of AI and should be verified against the official documentation before being relied on in production. The reference material consulted while preparing these pages is listed in the section bibliography. |
This page is written against Rust 1.99.0 and the 2024 edition, checked on 2026-10-06. Rust has no general reflection,
but the standard library’s std::any module offers a narrow form of run-time type information: you can ask a value for
its TypeId and attempt to downcast a type-erased value back to a concrete type. This page shows the API, the
classic patterns built on it, and when an enum is the better choice.
Any and TypeId
Any is a trait implemented automatically for every type that is 'static (contains no non-'static
references). It has one method, type_id(), returning a TypeId — an opaque, comparable, hashable identifier unique
to each type in the program.
use std::any::{Any, TypeId, type_name, type_name_of_val};
fn describe(value: &dyn Any) -> &'static str {
if value.is::<String>() {
"a String"
} else if value.type_id() == TypeId::of::<u16>() {
"a u16"
} else {
"something else"
}
}
fn main() {
println!("{}", describe(&String::from("Dune"))); // a String
println!("{}", describe(&1965u16)); // a u16
println!("{}", describe(&4.5f32)); // something else
println!("{}", type_name::<Vec<Option<u8>>>()); // alloc::vec::Vec<core::option::Option<u8>>
let rating = 4.5_f64;
println!("{}", type_name_of_val(&rating)); // f64
}
Source: std::any
type_name and type_name_of_val (1.76) return a human-readable name for debugging and logging only: the exact
string is not guaranteed to be stable between compiler versions and must not be used for logic. Use TypeId for
comparisons.
Downcasting
A &dyn Any can be converted back to a reference to its concrete type with downcast_ref::<T>(), which returns
Option<&T> (None if the type does not match). downcast_mut does the same for &mut, and
Box<dyn Any>::downcast::<T>() returns Result<Box<T>, Box<dyn Any>>, giving the box back on failure:
use std::any::Any;
fn bump_if_counter(value: &mut dyn Any) {
if let Some(n) = value.downcast_mut::<u32>() {
*n += 1;
}
}
fn main() {
let mut values: Vec<Box<dyn Any>> = vec![Box::new(1u32), Box::new("Dune"), Box::new(String::from("Emma"))];
for v in &mut values {
bump_if_counter(v.as_mut());
}
for v in &values {
if let Some(n) = v.downcast_ref::<u32>() {
println!("u32 {n}");
} else if let Some(s) = v.downcast_ref::<&str>() {
println!("&str {s}");
} else {
println!("unknown type"); // String is not &str
}
}
let boxed: Box<dyn Any> = values.pop().unwrap();
match boxed.downcast::<String>() {
Ok(owned) => println!("got back an owned String: {owned}"),
Err(other) => println!("not a String: {:?}", (*other).type_id()), // id of the value, not the Box
}
}
Source: std::any::Any
Downcasting matches the exact type: &str is not String, u32 is not u64, and a Box<dyn Any> holding a
Box<T> is not a T. dyn Any + Send and dyn Any + Send + Sync have the same methods, for values that cross
threads — std::thread::JoinHandle::join returns the panic payload as Box<dyn Any + Send>.
The 'static requirement
Any is only implemented for 'static types, because a TypeId cannot distinguish lifetimes: &'a str and
&'static str would share a TypeId, and downcasting would let you extend a lifetime unsoundly. So a struct holding a
borrowed &'a str cannot be put into a Box<dyn Any>; the compiler reports that the borrowed value "does not live long
enough" or that the type "may not live long enough". Store owned data (String, Arc<str>) in type-erased
containers.
From your trait to Any: as_any and trait upcasting
A common need: you have a &dyn Plugin (your own trait) and want to recover the concrete plugin type. Before Rust
1.86, there was no way to convert &dyn Plugin into &dyn Any, so every implementation had to provide an as_any
method — the pattern Azam’s Advanced Rust (chapter 8) teaches:
use std::any::Any;
// Pre-1.86 pattern: boilerplate in every implementation.
trait PluginOld {
fn name(&self) -> &str;
fn as_any(&self) -> &dyn Any;
}
struct Isbn10Upgrader;
impl PluginOld for Isbn10Upgrader {
fn name(&self) -> &str {
"isbn10"
}
fn as_any(&self) -> &dyn Any {
self
}
}
fn main() {
let p: Box<dyn PluginOld> = Box::new(Isbn10Upgrader);
println!("{} {}", p.name(), p.as_any().is::<Isbn10Upgrader>());
}
Source: std::any::Any
With trait upcasting (1.86), make Any a supertrait and coerce directly; no method is needed:
use std::any::Any;
trait Plugin: Any {
fn name(&self) -> &str;
}
struct Isbn10Upgrader {
converted: u32,
}
impl Plugin for Isbn10Upgrader {
fn name(&self) -> &str {
"isbn10"
}
}
fn main() {
let plugins: Vec<Box<dyn Plugin>> = vec![Box::new(Isbn10Upgrader { converted: 12 })];
for p in &plugins {
let any: &dyn Any = p.as_ref(); // trait upcasting: &dyn Plugin -> &dyn Any
if let Some(up) = any.downcast_ref::<Isbn10Upgrader>() {
println!("{} converted {}", p.name(), up.converted);
}
}
}
A pitfall to avoid: calling p.type_id() directly on a Box<dyn Plugin> (with Any in scope) returns the TypeId of
the box, not of the plugin, because Box<dyn Plugin> is itself 'static. Upcast first, then ask ((&**p as &dyn
Any).type_id(), or use downcast_ref as above).
A typed event-handler registry
TypeId as a HashMap key lets you build type maps: one entry per type, with type-safe access. The shelf
catalogue uses one to dispatch domain events to handlers registered for each event type:
use std::any::{Any, TypeId};
use std::collections::HashMap;
#[derive(Debug)]
struct BookLent {
isbn: String,
member: String,
}
#[derive(Debug)]
struct BookReturned {
isbn: String,
}
type Handler = Box<dyn Fn(&dyn Any)>;
#[derive(Default)]
struct EventBus {
handlers: HashMap<TypeId, Vec<Handler>>,
}
impl EventBus {
fn subscribe<E: Any>(&mut self, f: impl Fn(&E) + 'static) {
let wrapped: Handler = Box::new(move |event| {
if let Some(e) = event.downcast_ref::<E>() {
f(e); // the downcast cannot fail: the map is keyed by E's TypeId
}
});
self.handlers.entry(TypeId::of::<E>()).or_default().push(wrapped);
}
fn publish<E: Any>(&self, event: E) {
if let Some(list) = self.handlers.get(&TypeId::of::<E>()) {
for h in list {
h(&event);
}
}
}
}
fn main() {
let mut bus = EventBus::default();
bus.subscribe(|e: &BookLent| println!("notify {} about {}", e.member, e.isbn));
bus.subscribe(|e: &BookReturned| println!("reshelve {}", e.isbn));
bus.publish(BookLent { isbn: "978".into(), member: "alice".into() });
bus.publish(BookReturned { isbn: "978".into() });
bus.publish(42u8); // no subscribers: ignored
}
Source: std::any::TypeId
The same technique underlies request extensions in the http crate (http::Extensions, used by axum’s
Extension extractor), Bevy’s resources, and crates such as anymap.
Downcasting errors
std::error::Error has its own downcasting methods (downcast_ref, downcast_mut, downcast on
Box<dyn Error>), so code that receives a Box<dyn Error + Send + Sync> or an anyhow::Error can recover a specific
error type:
use std::error::Error;
use std::num::ParseIntError;
fn parse(text: &str) -> Result<u16, Box<dyn Error + Send + Sync>> {
Ok(text.parse::<u16>()?)
}
fn main() {
if let Err(e) = parse("19x5")
&& let Some(pe) = e.downcast_ref::<ParseIntError>()
{
println!("bad number: {pe}");
}
}
Source: std::error::Error::downcast_ref
When downcasting is a design smell
Downcasting gives up the compiler’s exhaustiveness checking: when a new type is added, nothing tells you which
downcast_ref chains need updating, and a typo in the type simply makes the branch never match. Prefer:
-
an
enumwhen the set of types is known (the usual case inside one crate) —matchis exhaustive; -
a trait method when the behaviour can be expressed polymorphically (
plugin.run()instead of "if it is anIsbn10Upgrader, do X"); -
generics when the type is known at compile time.
Reserve Any for genuinely open-ended, type-keyed storage (event buses, extension maps, plugin registries across crate
boundaries) and for inspecting panic payloads and boxed errors.
Where the books are outdated
Azam (Advanced Rust, chapter 8) presents Any, downcast_ref/downcast_mut, the as_any pattern and an
event-handler case, and TypeId. Since Rust 1.86, trait upcasting removes the need for as_any; the rest of the
chapter’s material is consistent with the current std::any documentation.