Type Conversions & Coercions
|
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 never converts between
types silently in general — let x: u64 = some_u32; is a type error — but it does apply a small set of well-defined
coercions automatically, and the standard library defines conversion traits that make explicit conversions uniform.
This page covers both, plus the algorithm that makes value.method() work on references and smart pointers.
From and Into
From<T> for U defines an infallible conversion from T to U. Implementing From gives you the reciprocal Into
for free through a blanket impl, so implement From, accept Into:
#[derive(Debug)]
struct Isbn(String);
#[derive(Debug)]
struct Author {
name: String,
}
impl From<&str> for Author {
fn from(name: &str) -> Self {
Author { name: name.to_owned() }
}
}
impl From<Isbn> for String {
fn from(isbn: Isbn) -> Self {
isbn.0
}
}
fn add_author(name: impl Into<String>) -> String { // accepts &str, String, Box<str>, Cow<str>, ...
let name: String = name.into();
format!("added {name}")
}
fn main() {
let a = Author::from("Octavia Butler");
let b: Author = "Ursula K. Le Guin".into();
let raw: String = Isbn("9780441013593".into()).into();
let wide = u64::from(42u32); // lossless numeric conversions use From
let as_f64 = f64::from(1.5f32);
println!("{a:?} {b:?} {raw} {wide} {as_f64}");
println!("{} {}", add_author("Le Guin"), add_author(String::from("Butler")));
}
Source: Rust by Example: From and Into
From must not fail and should not lose information. It also powers error conversion in ? (see
Error Handling). Lossless numeric conversions are From
(u8 → u32, i16 → f32); lossy ones are not.
TryFrom and TryInto
For conversions that can fail, implement TryFrom with an Error type; TryInto comes for free. The standard library
uses it for narrowing integer conversions and slice-to-array conversions:
use std::convert::TryFrom;
#[derive(Debug, Clone, Copy, PartialEq)]
struct Rating(u8);
impl TryFrom<u32> for Rating {
type Error = String;
fn try_from(value: u32) -> Result<Self, Self::Error> {
match value {
1..=5 => Ok(Rating(value as u8)),
_ => Err(format!("rating must be 1-5, got {value}")),
}
}
}
fn main() {
println!("{:?} {:?}", Rating::try_from(4), Rating::try_from(9));
let r: Result<Rating, _> = 3u32.try_into();
println!("{r:?}");
println!("{:?}", u8::try_from(300i32)); // Err(TryFromIntError(PosOverflow))
let digits: [u8; 3] = vec![9, 7, 8].try_into().expect("exactly three");
println!("{digits:?}");
}
AsRef, AsMut, Borrow and ToOwned
These traits express cheap reference-to-reference conversions:
| Trait | Use |
|---|---|
|
"can be viewed as a |
|
like |
|
the generalisation of |
use std::collections::HashMap;
use std::path::Path;
fn file_size(path: impl AsRef<Path>) -> u64 {
std::fs::metadata(path.as_ref()).map(|m| m.len()).unwrap_or(0)
}
fn shout<S: AsRef<str>>(s: S) -> String {
s.as_ref().to_uppercase()
}
fn main() {
println!("{} {}", file_size("/etc/hostname"), file_size(std::path::PathBuf::from("/nope")));
println!("{} {}", shout("dune"), shout(String::from("emma")));
let stock: HashMap<String, u32> = HashMap::from([("Dune".to_string(), 2)]);
println!("{:?}", stock.get("Dune")); // Borrow<str> for String: no allocation to look up
let owned: String = "Kindred".to_owned(); // ToOwned
println!("{owned}");
}
Source: std::convert::AsRef
FromStr
FromStr parses a value from a string slice; str::parse::<T>() calls it. Implement it for your own types alongside
Display (see Strings & Text for an example).
as casts
as performs primitive casts between numeric types, bool/char and integers, and pointer types. It never fails at
run time, which is exactly its danger: narrowing integer casts truncate, float-to-integer casts saturate, and
sign changes reinterpret bits. Prefer From for widening and TryFrom for narrowing; use as when truncation is
intended (hashing, bit manipulation) or for pointer and FFI work.
fn main() {
let big: i64 = -1;
println!("{}", big as u8); // 255 -- reinterpreted bits
println!("{}", 70_000u32 as u16); // 4464 -- truncated
println!("{}", 1e10_f64 as i32); // 2147483647 -- saturated
println!("{}", u8::try_from(big).is_err()); // true -- the checked alternative
}
Coercions
A coercion is an implicit conversion that the compiler inserts at specific places, called coercion sites:
-
letstatements with an explicit type (let s: &str = &string;); -
staticandconstitems; -
function and method call arguments;
-
struct, enum-variant and array element expressions;
-
function results (the tail expression or a
return).
Outside coercion sites — for example in generic arguments being inferred, or on the left of a method call — no coercion happens, which explains many confusing E0308 errors.
Kinds of coercion
| Coercion | From → To | Example |
|---|---|---|
Reference weakening |
|
pass |
Raw pointer |
|
FFI calls |
Deref coercion |
|
|
Unsizing |
|
|
Trait upcasting (1.86) |
|
|
Function item → pointer |
|
storing functions in a table |
Non-capturing closure → pointer |
closure → |
callbacks |
Never |
|
|
use std::fmt::Debug;
use std::rc::Rc;
fn takes_str(s: &str) -> usize {
s.len()
}
fn takes_slice(v: &[u16]) -> usize {
v.len()
}
fn takes_debug(d: &dyn Debug) -> String {
format!("{d:?}")
}
fn main() {
let owned = String::from("Dune");
let boxed: Box<String> = Box::new(owned.clone());
let rc: Rc<String> = Rc::new(owned.clone());
println!("{}", takes_str(&owned)); // &String -> &str (Deref)
println!("{}", takes_str(&boxed)); // &Box<String> -> &String -> &str (Deref twice)
println!("{}", takes_str(&rc)); // &Rc<String> -> &String -> &str
let years = [1965u16, 1815];
println!("{}", takes_slice(&years)); // &[u16; 2] -> &[u16] (unsizing)
let mut v = vec![1, 2];
let exclusive: &mut Vec<i32> = &mut v;
println!("{}", takes_debug(exclusive)); // &mut Vec -> &Vec (weakening) -> &dyn Debug (unsizing)
}
Deref coercion and custom Deref
Implementing Deref<Target = T> for your type lets &YourType coerce to &T and gives it all the &self methods of T
through auto-deref. This is designed for smart pointers (Box, Rc, String, Vec). Using Deref on a
newtype to "inherit" the inner type’s methods is a known anti-pattern (it emulates inheritance, surprises readers and
leaks the inner type’s API); implement the methods you need, or AsRef, instead.
use std::ops::Deref;
/// A smart pointer that counts how often its contents are read.
struct Tracked<T> {
value: T,
reads: std::cell::Cell<u32>,
}
impl<T> Deref for Tracked<T> {
type Target = T;
fn deref(&self) -> &T {
self.reads.set(self.reads.get() + 1);
&self.value
}
}
fn main() {
let title = Tracked { value: String::from("Dune"), reads: Default::default() };
println!("{} {}", title.len(), title.to_uppercase()); // String methods via auto-deref
let s: &str = &title; // &Tracked<String> -> &String -> &str
println!("{s} -- read {} times", title.reads.get());
}
DerefMut does the same for &mut. Deref coercion is transitive — the compiler applies deref as many times as
needed, as &Box<String> → &String → &str showed — but each step must be a Deref impl; it never invents
conversions through From.
Least-upper-bound coercion
When several expressions must have one common type — the branches of an if or match, the elements of an array
literal — the compiler tries to find a type that all of them coerce to. Function items, for instance, all become a
function pointer, and references to different types implementing a trait can be unified by annotating the target:
use std::fmt::Display;
fn by_title() -> &'static str {
"title"
}
fn by_year() -> &'static str {
"year"
}
fn main() {
let descending = true;
let pick = if descending { by_title } else { by_year }; // two fn items -> fn() -> &'static str
println!("{}", pick());
let fields = [by_title, by_year]; // array of fn pointers
println!("{}", fields.len());
let label: &dyn Display = if descending { &1965 } else { &"unknown" }; // annotated target
println!("{label}");
}
Without the annotation on label, the two branches have types &i32 and &&str, and there is no least upper bound,
so the compiler reports E0308 ("`if` and else have incompatible types").
Coercions and generics
Coercions do not happen when a generic type parameter is being inferred: passing &String to fn f<T: Display + ?Sized>(x: &T)
infers T = String, not str. And a trait implemented for &str is not automatically available for &String in a
trait-bound check (E0277), because trait matching does not apply coercions; call .as_str() or implement the trait for
both. Azam (Advanced Rust, chapter 4) discusses these cases in detail.
The method-call algorithm: auto-ref and auto-deref
For receiver.method(), the compiler searches for a matching method by trying, in order, the receiver type T, then
&T, then &mut T; then it dereferences (*T) and repeats; finally it tries an unsizing step (array to slice). The
first type that has a method with that name (inherent methods before trait methods at each step) wins:
use std::rc::Rc;
fn main() {
let titles: Rc<Vec<String>> = Rc::new(vec!["Dune".into(), "Emma".into()]);
// len: Rc<Vec<String>> -> &Rc -> &mut Rc -> deref to Vec<String> -> &Vec: found Vec::len
println!("{}", titles.len());
// first: ... Vec<String> -> deref to [String] -> &[String]: found slice::first
println!("{:?}", titles.first());
// to_uppercase on the first element: &String -> deref to str -> &str: found str::to_uppercase
println!("{}", titles[0].to_uppercase());
// clone: Rc<Vec> has `clone` (via Clone) at the first step, so this clones the Rc, not the Vec
let c = titles.clone();
println!("{}", Rc::strong_count(&c)); // 2
}
The last line is why the idiom Rc::clone(&titles) is preferred: it makes clear that only the pointer is cloned. The
Rustonomicon’s "The Dot Operator" chapter walks through more cases, including why (&x).clone() may return a
reference.
transmute
std::mem::transmute reinterprets the bits of one type as another of the same size. It is unsafe, bypasses every
check on this page, and is almost never the right tool — from_le_bytes, to_bits/from_bits, as, pointer casts
and bytemuck-style safe transmutes cover nearly every use. See Unsafe
Rust.
Common errors
| Error | Cause and fix |
|---|---|
E0308 mismatched types |
no coercion applies at that position:
convert explicitly ( |
E0277 |
implement |
E0605 non-primitive cast |
|
E0282 type annotations needed after |
|
Where the books are outdated
Azam (Advanced Rust, chapter 4) covers Deref coercion including custom Deref, coercion sites, reference, function
item and trait-object coercions, coercions in generics, transitivity and least-upper-bound coercion. The only notable
change since then is trait upcasting (1.86), a new unsizing coercion from one trait object to a supertrait object.
References
-
Rust by Example: Conversion (From and Into, TryFrom and TryInto, To and from Strings)
-
The Rust Programming Language: Treating Smart Pointers Like Regular References with Deref
-
Rust Design Patterns: Deref polymorphism anti-pattern (community)