How do I work with TypeId in Rust?

Walkthrough

TypeId represents a globally unique identifier for a type. It's part of Rust's std::any module and enables runtime type identification, which is useful for implementing dynamic typing patterns, type erasure, and trait object downcasting.

Key concepts:

  • Globally unique — Each type has exactly one TypeId across the entire program
  • Const constructible — Can be computed at compile time with TypeId::of::<T>()
  • Eq + Hash — Can be used as keys in HashMaps and HashSets
  • 'static — Only works with 'static types (no lifetimes)

When to use TypeId:

  • Implementing downcasting for trait objects
  • Building type registries or maps
  • Runtime type checking
  • Implementing custom Any-like types
  • Debug/logging for generic code

Limitations:

  • Only works with types that have 'static lifetime
  • TypeId values may change between compilations
  • Not stable across different Rust versions

Code Examples

Basic TypeId Usage

use std::any::TypeId;
 
fn main() {
    // Get TypeId for different types
    let type_id_i32 = TypeId::of::<i32>();
    let type_id_string = TypeId::of::<String>();
    let type_id_vec = TypeId::of::<Vec<i32>>();
    
    println!("TypeId of i32: {:?}", type_id_i32);
    println!("TypeId of String: {:?}", type_id_string);
    println!("TypeId of Vec<i32>: {:?}", type_id_vec);
    
    // Compare TypeIds
    let same_type = TypeId::of::<i32>();
    assert_eq!(type_id_i32, same_type);
    
    // Different types have different TypeIds
    assert_ne!(type_id_i32, type_id_string);
}

Type Checking with TypeId

use std::any::{Any, TypeId};
 
fn is_type<T: 'static>(value: &dyn Any) -> bool {
    value.type_id() == TypeId::of::<T>()
}
 
fn main() {
    let value: i32 = 42;
    let any_ref: &dyn Any = &value;
    
    println!("Is i32: {}", is_type::<i32>(any_ref));
    println!("Is String: {}", is_type::<String>(any_ref));
    println!("Is u32: {}", is_type::<u32>(any_ref));
}

TypeId in Collections

use std::any::TypeId;
use std::collections::HashMap;
 
struct TypeRegistry {
    data: HashMap<TypeId, String>,
}
 
impl TypeRegistry {
    fn new() -> Self {
        Self {
            data: HashMap::new(),
        }
    }
    
    fn register<T: 'static>(&mut self, description: &str) {
        self.data.insert(TypeId::of::<T>(), description.to_string());
    }
    
    fn describe<T: 'static>(&self) -> Option<&str> {
        self.data.get(&TypeId::of::<T>()).map(|s| s.as_str())
    }
}
 
fn main() {
    let mut registry = TypeRegistry::new();
    
    registry.register::<i32>("32-bit signed integer");
    registry.register::<String>("Heap-allocated string");
    registry.register::<Vec<u8>>("Byte vector");
    
    println!("i32: {}", registry.describe::<i32>().unwrap());
    println!("String: {}", registry.describe::<String>().unwrap());
    println!("f64: {:?}", registry.describe::<f64>());
}

Custom Any Implementation

use std::any::TypeId;
use std::collections::HashMap;
 
// Type-erased storage with TypeId keys
struct TypeMap {
    data: HashMap<TypeId, Box<dyn std::any::Any>>,
}
 
impl TypeMap {
    fn new() -> Self {
        Self {
            data: HashMap::new(),
        }
    }
    
    fn insert<T: 'static>(&mut self, value: T) {
        self.data.insert(TypeId::of::<T>(), Box::new(value));
    }
    
    fn get<T: 'static>(&self) -> Option<&T> {
        self.data
            .get(&TypeId::of::<T>())
            .and_then(|v| v.downcast_ref::<T>())
    }
    
    fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
        self.data
            .get_mut(&TypeId::of::<T>())
            .and_then(|v| v.downcast_mut::<T>())
    }
    
    fn remove<T: 'static>(&mut self) -> Option<T> {
        self.data
            .remove(&TypeId::of::<T>())
            .and_then(|v| v.downcast::<T>().ok())
    }
}
 
fn main() {
    let mut map = TypeMap::new();
    
    map.insert(42i32);
    map.insert(String::from("Hello"));
    map.insert(vec![1.0, 2.0, 3.0f64]);
    
    println!("i32: {}", map.get::<i32>().unwrap());
    println!("String: {}", map.get::<String>().unwrap());
    println!("Vec<f64>: {:?}", map.get::<Vec<f64>>().unwrap());
    
    // Modify a value
    if let Some(s) = map.get_mut::<String>() {
        s.push_str(", World!");
    }
    println!("Modified: {}", map.get::<String>().unwrap());
}

Downcasting Pattern

use std::any::{Any, TypeId};
 
trait Widget: Any {
    fn draw(&self);
    fn as_any(&self) -> &dyn Any;
    fn as_any_mut(&mut self) -> &mut dyn Any;
}
 
// Helper to get TypeId from dyn Widget
fn widget_type_id(widget: &dyn Widget) -> TypeId {
    widget.as_any().type_id()
}
 
struct Button {
    label: String,
}
 
impl Widget for Button {
    fn draw(&self) {
        println!("Button: {}", self.label);
    }
    
    fn as_any(&self) -> &dyn Any {
        self
    }
    
    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }
}
 
struct TextBox {
    content: String,
}
 
impl Widget for TextBox {
    fn draw(&self) {
        println!("TextBox: {}", self.content);
    }
    
    fn as_any(&self) -> &dyn Any {
        self
    }
    
    fn as_any_mut(&mut self) -> &mut dyn Any {
        self
    }
}
 
fn main() {
    let widgets: Vec<Box<dyn Widget>> = vec![
        Box::new(Button { label: String::from("Click me") }),
        Box::new(TextBox { content: String::from("Hello") }),
    ];
    
    for widget in &widgets {
        widget.draw();
        
        // Check specific type
        if widget.as_any().is::<Button>() {
            println!("  -> This is a button!");
        }
    }
    
    // Downcast to specific type
    if let Some(button) = widgets[0].as_any().downcast_ref::<Button>() {
        println!("Button label: {}", button.label);
    }
}

TypeId for Debugging

use std::any::{type_name, TypeId};
 
fn debug_type_info<T: 'static>(value: &T) {
    println!("Type name: {}", type_name::<T>());
    println!("Type ID: {:?}", TypeId::of::<T>());
    println!("Size: {} bytes", std::mem::size_of::<T>());
    println!("Alignment: {} bytes", std::mem::align_of::<T>());
}
 
fn main() {
    debug_type_info(&42i32);
    println!();
    debug_type_info(&String::from("hello"));
    println!();
    debug_type_info(&vec![1, 2, 3]);
}

Const TypeId Usage

use std::any::TypeId;
 
// TypeId::of is const fn, so can be used in const contexts
const I32_TYPE_ID: TypeId = TypeId::of::<i32>();
const STRING_TYPE_ID: TypeId = TypeId::of::<String>();
 
fn is_i32_type(id: TypeId) -> bool {
    id == I32_TYPE_ID
}
 
fn main() {
    println!("i32 TypeId: {:?}", I32_TYPE_ID);
    println!("String TypeId: {:?}", STRING_TYPE_ID);
    
    let id = TypeId::of::<i32>();
    println!("Is i32: {}", is_i32_type(id));
}

Type-Indexed Cache

use std::any::TypeId;
use std::collections::HashMap;
use std::time::Instant;
 
struct Cache {
    values: HashMap<TypeId, (Box<dyn std::any::Any>, Instant)>,
}
 
impl Cache {
    fn new() -> Self {
        Self {
            values: HashMap::new(),
        }
    }
    
    fn get_or_insert<T: 'static>(&mut self, f: impl FnOnce() -> T) -> &T {
        let key = TypeId::of::<T>();
        if !self.values.contains_key(&key) {
            let value = Box::new(f());
            self.values.insert(key, (value, Instant::now()));
        }
        
        self.values
            .get(&key)
            .and_then(|(v, _)| v.downcast_ref::<T>())
            .unwrap()
    }
    
    fn clear<T: 'static>(&mut self) {
        self.values.remove(&TypeId::of::<T>());
    }
    
    fn clear_all(&mut self) {
        self.values.clear();
    }
}
 
fn main() {
    let mut cache = Cache::new();
    
    // First access computes the value
    let val1 = cache.get_or_insert(|| {
        println!("Computing expensive value...");
        42
    });
    println!("Got: {}", val1);
    
    // Second access uses cached value
    let val2 = cache.get_or_insert(|| {
        println!("This won't print");
        99
    });
    println!("Got again: {}", val2);
}

Multiple Trait Objects with TypeId

use std::any::{Any, TypeId};
use std::collections::HashMap;
 
// Event handling with typed events
trait Event: Any {
    fn type_name(&self) -> &'static str;
}
 
struct ClickEvent {
    x: i32,
    y: i32,
}
 
impl Event for ClickEvent {
    fn type_name(&self) -> &'static str {
        "ClickEvent"
    }
}
 
struct KeyEvent {
    key: char,
}
 
impl Event for KeyEvent {
    fn type_name(&self) -> &'static str {
        "KeyEvent"
    }
}
 
struct EventHandler {
    handlers: HashMap<TypeId, Box<dyn Fn(&dyn Any)>>,
}
 
impl EventHandler {
    fn new() -> Self {
        Self {
            handlers: HashMap::new(),
        }
    }
    
    fn on<E: Event + 'static>(&mut self, handler: impl Fn(&E) + 'static) {
        let key = TypeId::of::<E>();
        self.handlers.insert(key, Box::new(move |event: &dyn Any| {
            if let Some(e) = event.downcast_ref::<E>() {
                handler(e);
            }
        }));
    }
    
    fn dispatch(&self, event: &dyn Event) {
        if let Some(handler) = self.handlers.get(&event.type_id()) {
            handler(event.as_any());
        }
    }
}
 
impl dyn Event {
    fn as_any(&self) -> &dyn Any {
        // This requires Self: Any, which is satisfied
        unsafe { &*(self as *const Self as *const dyn Any) }
    }
}
 
fn main() {
    let mut handler = EventHandler::new();
    
    handler.on(|e: &ClickEvent| {
        println!("Click at ({}, {})", e.x, e.y);
    });
    
    handler.on(|e: &KeyEvent| {
        println!("Key pressed: {}", e.key);
    });
    
    // Dispatch events
    let click = ClickEvent { x: 100, y: 200 };
    let key = KeyEvent { key: 'a' };
    
    handler.dispatch(&click);
    handler.dispatch(&key);
}

TypeId Equality Checking

use std::any::TypeId;
 
fn main() {
    // Same type always has same TypeId
    assert_eq!(TypeId::of::<i32>(), TypeId::of::<i32>());
    assert_eq!(TypeId::of::<String>(), TypeId::of::<String>());
    
    // Different types have different TypeIds
    assert_ne!(TypeId::of::<i32>(), TypeId::of::<u32>());
    assert_ne!(TypeId::of::<Vec<i32>>(), TypeId::of::<Vec<u32>>());
    
    // Type aliases don't create new TypeIds
    type MyInt = i32;
    assert_eq!(TypeId::of::<i32>(), TypeId::of::<MyInt>());
    
    println!("All assertions passed!");
}

Type-Scoped IDs

use std::any::TypeId;
use std::marker::PhantomData;
 
// Type-scoped identifier
struct TypedId<T: 'static> {
    id: u64,
    _marker: PhantomData<T>,
}
 
impl<T: 'static> TypedId<T> {
    fn new(id: u64) -> Self {
        Self {
            id,
            _marker: PhantomData,
        }
    }
    
    fn type_id(&self) -> TypeId {
        TypeId::of::<T>()
    }
}
 
// Verify type at runtime
fn verify_type<T: 'static>(id: &TypedId<T>) -> bool {
    id.type_id() == TypeId::of::<T>()
}
 
fn main() {
    let int_id = TypedId::<i32>::new(1);
    let str_id = TypedId::<String>::new(2);
    
    println!("int_id type: {:?}", int_id.type_id());
    println!("str_id type: {:?}", str_id.type_id());
    
    println!("int_id verified: {}", verify_type(&int_id));
}

TypeId Hash and Equality

use std::any::TypeId;
use std::collections::{HashMap, HashSet};
 
fn main() {
    // TypeId implements Hash and Eq
    let mut set = HashSet::new();
    set.insert(TypeId::of::<i32>());
    set.insert(TypeId::of::<String>());
    set.insert(TypeId::of::<Vec<u8>>());
    
    println!("Set contains i32: {}", set.contains(&TypeId::of::<i32>()));
    println!("Set contains f64: {}", set.contains(&TypeId::of::<f64>()));
    
    // Use in HashMap
    let mut type_sizes: HashMap<TypeId, usize> = HashMap::new();
    type_sizes.insert(TypeId::of::<i32>(), std::mem::size_of::<i32>());
    type_sizes.insert(TypeId::of::<String>(), std::mem::size_of::<String>());
    
    println!("Size of i32: {}", type_sizes[&TypeId::of::<i32>()]);
    println!("Size of String: {}", type_sizes[&TypeId::of::<String>()]);
}

Limitations and Edge Cases

use std::any::TypeId;
 
// This won't compile - types with lifetimes can't have TypeId
// fn get_lifetime_type_id<T>() -> TypeId {
//     TypeId::of::<&'a T>() // Error: lifetime parameter 'a
// }
 
fn main() {
    // &str has a lifetime, but the reference type itself is 'static
    // when it points to a 'static str
    let static_str: &'static str = "hello";
    let str_ref_id = TypeId::of::<&'static str>();
    println!("&'static str TypeId: {:?}", str_ref_id);
    
    // Different lifetime = different type = different TypeId
    // TypeId::of::<&'a str>() where 'a != 'static would be different
    // But we can't demonstrate this without named lifetime parameters
    
    // Generic type with different type arguments
    assert_ne!(TypeId::of::<Vec<i32>>(), TypeId::of::<Vec<String>>());
    
    // Closure types are unique (different TypeIds for different closures)
    let closure1 = || 1;
    let closure2 = || 1;
    // These have different types even though they look the same!
    // TypeId::of::<typeof(closure1)>() != TypeId::of::<typeof(closure2)>()
}

Summary

TypeId Characteristics:

Property Description
Uniqueness Each type has exactly one TypeId
Stability May change between compilations
Constraints Only 'static types can have TypeId
Traits Clone, Copy, Eq, Hash

Key Functions:

Function Description
TypeId::of::<T>() Get TypeId for type T (const fn)
any.type_id() Get TypeId from dyn Any
type_name::<T>() Get type name as string (debug)

Comparison with Other Languages:

Language Feature Rust Equivalent
Java instanceof Any::downcast_ref + TypeId
C++ typeid TypeId::of
C# typeof TypeId::of + type_name
Go reflect.TypeOf TypeId::of

When to Use TypeId:

Scenario Appropriate?
Downcasting trait objects āœ… Yes (via Any)
Type registries āœ… Yes
Runtime type checking āœ… Yes
Type-indexed storage āœ… Yes
Normal generic code āŒ Use generics directly
Persistent serialization āŒ Not stable

Key Points:

  • TypeId provides a unique identifier for each 'static type
  • Works with Any trait for runtime type identification
  • Can be used as keys in HashMaps and HashSets
  • TypeId::of::<T>() is a const fn
  • Values may change between compilations (not for serialization)
  • Type aliases share the same TypeId as their underlying type
  • Essential for implementing type erasure and downcasting
  • Combine with type_name for debugging