How to print the type of a variable in Rust without losing your mind
\n\nHave you ever been writing Rust code and suddenly frozen with the question: "What type is this variable?" If so — you are not alone. The question "How do I print the type of a variable in Rust" on Stack Overflow has gathered thousands of views, and for good reason. Rust is a language with a strict but smart type system, and sometimes the compiler knows more than we do.
\n\nIn this article, we will cover three ways to print the type of a variable: from a simple debugging trick to a professional macro. You will learn when to use which method and stop guessing what beast is hiding behind let x = ....
Why doesn't Rust just let you print the type?
\n\nIn languages like Python or JavaScript, you can write print(type(x)) — and voilà. Rust doesn't have this luxury for two reasons:
- \n
- Static typing: types are known at compile time, but are not always obvious to the programmer (especially when working with closures or complex generics). \n
- Lack of reflection: Rust does not store type information at runtime by default — this would impact performance. \n
But the Rust developers have left us a few loopholes. Let's take a look at them.
\n\nMethod 1: std::any::type_name — the simplest (but not perfect)
\n\nThe Rust standard library provides the std::any::type_name function. It returns a string with the type name. Here's how it works:
use std::any::type_name;\n\nfn print_type<T>(_: &T) {\n println!("{}", type_name::<T>());\n}\n\nfn main() {\n let x = 42;\n let y = "hello";\n let z = vec![1, 2, 3];\n \n print_type(&x); // i32\n print_type(&y); // &str\n print_type(&z); // Vec<i32>\n}\n\nPros:
\n- \n
- Minimal code — works out of the box. \n
- Suitable for debugging and quick checks. \n
Cons:
\n- \n
- The result depends on the compiler: the string may differ across Rust versions or platforms. \n
- Cannot be used in production logic where decisions need to be made based on the type. \n
Live example: debugging a complex expression
\n\nImagine you are working with iterators and method chains. What type will you end up with?
\n\nuse std::any::type_name;\n\nfn main() {\n let data = vec![1, 2, 3, 4, 5];\n let result = data.iter()\n .filter(|x| *x % 2 == 0)\n .map(|x| x * 2)\n .collect::<Vec<_>>();\n \n // Want to know the type before collect?\n let intermediate = data.iter()\n .filter(|x| *x % 2 == 0)\n .map(|x| x * 2);\n \n println!("Type of intermediate: {}", type_name::<decltype!(intermediate)>()); \n // Oops, that won't work — need to pass a reference\n println!("Type of intermediate: {}", type_name_of_val(&intermediate));\n}\n\nfn type_name_of_val<T>(_: &T) -> &str {\n type_name::<T>()\n}\n\nThis trick will save you when the compiler gives an error and you don't understand what type it expects.
\n\nMethod 2: Using compiler errors — a clever trick
\n\nSometimes the best way to find out the type is to make the compiler tell you itself. There is an old but working trick: assign the variable to a non-existent type.
\n\nfn main() {\n let x = 42;\n let y: () = x; // Error! expected (), found i32\n}\n\nThe compiler will output something like:
\nerror[E0308]: mismatched types\n --> src/main.rs:3:18\n |\n3 | let y: () = x;\n | -- ^ expected `()`, found `i32`\n | |\n | expected due to this\n\nPros:
\n- \n
- No imports required. \n
- Works even in environments without std (no_std).