Rust Deref and AsRef Explained
Demystify Rust's Deref, DerefMut, and AsRef traits with clear examples showing how smart pointers, coercions, and flexible APIs really work.
What you'll learn
- ✓How Deref enables smart pointer ergonomics
- ✓The difference between Deref and AsRef
- ✓When deref coercion fires automatically
- ✓How to write flexible function signatures
- ✓When implementing Deref is a bad idea
Prerequisites
- •Basic Rust traits familiarity
What and Why
Two traits show up everywhere in idiomatic Rust APIs: Deref and AsRef. Both let you treat one type as if it were another, but they solve different problems. Deref is the foundation of smart pointers; it’s how Box<T> lets you call methods of T directly. AsRef is a conversion trait that makes function signatures flexible enough to accept many input types without forcing callers to convert manually.
Confusing them is a rite of passage. By the end of this article you’ll know exactly when each fires and how to use them in your own designs.
Mental Model
Deref says: “this type wraps another, and the compiler may transparently follow it.” When you write *boxed or call a method on boxed, the compiler walks through Deref to find the inner type. It can even chain: Box<String> derefs to String which derefs to str.
AsRef is a humble request: “give me a reference view of yourself, cheaply.” It does not insert anything automatically. You have to call .as_ref() or accept impl AsRef<Path> in a signature. There is no coercion magic.
A useful slogan: Deref is for transparency, AsRef is for flexibility.
Hands-on Example
Let’s see both in action with a tiny wrapper and a file-loading function.
use std::ops::Deref;
use std::path::{Path, PathBuf};
struct Username(String);
impl Deref for Username {
type Target = str;
fn deref(&self) -> &str {
&self.0
}
}
fn greet(name: &str) {
println!("hello {name}");
}
fn load<P: AsRef<Path>>(path: P) -> std::io::Result<String> {
std::fs::read_to_string(path.as_ref())
}
fn main() -> std::io::Result<()> {
let u = Username("ada".into());
greet(&u); // deref coercion: &Username -> &str
println!("{}", u.len()); // method from str via Deref
load("data.txt")?; // &str -> &Path via AsRef
load(String::from("a.txt"))?; // String -> &Path via AsRef
load(PathBuf::from("b.txt"))?; // PathBuf -> &Path via AsRef
Ok(())
}
greet(&u) works because &Username coerces to &str through Deref. The compiler inserts a call to deref for you. By contrast, load accepts anything implementing AsRef<Path>, and we explicitly call .as_ref() inside. The callers pass three different concrete types without thinking about it.
Common Pitfalls
Implementing Deref on domain types. Wrapping String in a Username and adding Deref<Target = str> feels clever, but it leaks string-ness everywhere. Now username.contains("admin") works, breaking encapsulation. Reserve Deref for genuine smart pointers (Box, Rc, Arc, Cow, custom owning pointers).
Forgetting DerefMut. If your wrapper needs mutable access, implement both. Otherwise users can read through the pointer but not modify, which is confusing.
Calling .as_ref() when a coercion exists. let s: &str = string.as_ref(); works, but let s: &str = &string; is shorter and idiomatic. Use AsRef in generic signatures, not at call sites.
Mixing up AsRef and Borrow. Borrow adds invariants (hash and ordering must match), which matters for HashMap keys. For plain “view as,” AsRef is the right tool.
Over-generic signatures. fn foo<S: AsRef<str>>(s: S) is great when callers actually pass diverse types. If 99% of callers pass &str, just take &str and keep the signature readable.
Practical Tips
When you write a function that opens files, prefer impl AsRef<Path>. Callers will thank you for accepting &str, String, PathBuf, and more without conversion noise.
When you write a newtype, ask “is this a smart pointer?” If yes, implement Deref. If it’s a domain concept (an email address, a user ID), expose explicit methods instead. The newtype’s purpose is to add invariants, not to be invisible.
Read the standard library for inspiration. Vec<T>: Deref<Target = [T]> is why slice methods work on vectors. String: Deref<Target = str> is why string-slice methods work on String. These are the canonical use cases.
Use &*x to force a deref to its target type when type inference goes sideways. This trick is occasionally useful when passing to generic code.
If a signature would need AsRef<str> + AsRef<Path>, you’ve probably mixed concerns. Split the function.
Wrap-up
Deref and AsRef cover two distinct ergonomics needs. Deref powers Rust’s smart-pointer story by letting the compiler transparently follow wrappers; use it sparingly and only when your type really is a pointer. AsRef powers flexible APIs by letting callers pass whatever type they have. Master the distinction and your Rust code will read like the standard library: terse at the call site, principled at the definition.
Related articles
- Rust Rust Iterators and Adapters
Master Rust's iterator trait, lazy adapters like map and filter, and consumers like collect and fold for fast, expressive data processing.
- Rust Rust Smart Pointers: Box, Rc, and Arc
Understand when to reach for Box, Rc, and Arc in Rust, how each interacts with the borrow checker, and the cost of shared ownership across threads.
- Rust Rust Traits vs Interfaces
Rust traits look like interfaces from other languages, but the differences matter: associated types, blanket impls, coherence, and dynamic dispatch all behave their own way.
- Rust Rust Traits: Shared Behavior Without Inheritance
Understand how Rust traits define shared behavior, with default methods, trait bounds, derive, and a clear take on dyn Trait versus impl Trait.