When a wrapper needs to disappear
You're writing a function that greets a user. It takes &str because that's the universal text reference in Rust. You have a Box<String> holding the name. You try to pass &my_box. The compiler rejects you with E0308 (mismatched types). You try &*my_box. It works, but now your code is littered with asterisks. You want the wrapper to just behave like the data inside, at least when you're reading it.
That's what Deref is for. It tells the compiler that a reference to your wrapper can automatically turn into a reference to the inner value. The wrapper becomes invisible to the caller.
The automatic look-inside rule
Deref is a trait in std::ops. When a type implements Deref, it promises it can hand out a reference to something else. The "something else" is called the target.
Think of a library card system. The card itself isn't the book. But the library has a rule: if you show the card, the librarian automatically fetches the book for you. You don't need to know where the book is stored. You just show the card, and you get the book.
In Rust, the card is your smart pointer or wrapper. The book is the inner data. Deref is the rule that lets the compiler fetch the book automatically whenever a function asks for it.
This only works for references. Deref turns &Wrapper into &Inner. It does not turn Wrapper into Inner. You can't move the value out of a wrapper using Deref. You can only borrow through it.
Minimal example
Here's a custom box that implements Deref. The code shows the trait definition and how coercion removes the need for manual dereferencing.
use std::ops::Deref;
/// A wrapper that holds any type T.
struct MyBox<T>(T);
/// Implement Deref so MyBox can act like T when borrowed.
impl<T> Deref for MyBox<T> {
/// The target type is the inner value.
type Target = T;
/// Return a reference to the inner value.
fn deref(&self) -> &Self::Target {
&self.0
}
}
/// A function that expects a string slice.
fn print_name(name: &str) {
println!("Name: {}", name);
}
fn main() {
// Create a box containing a String.
let box_str = MyBox(String::from("Rust"));
// The compiler coerces &MyBox<String> to &str automatically.
// No asterisk needed.
print_name(&box_str);
}
The call print_name(&box_str) works because the compiler inserts the deref calls for you. It sees you have &MyBox<String> and the function wants &str. It checks if MyBox implements Deref. It does. The target is String. The compiler pretends you wrote print_name(box_str.deref()). Now you have &String. The function still wants &str. The compiler checks if String implements Deref. It does. The target is str. The compiler inserts another call. You get &str. The types match.
What the compiler actually does
Deref coercion is not magic. It is code generation. The compiler inserts calls to the .deref() method at compile time. Those calls run at runtime.
This matters if your deref implementation has side effects. The side effects will happen every time coercion occurs.
The compiler follows a strict path. It looks at the type you have. It checks for Deref. If found, it moves to the target type. It repeats this process. Rust limits the chain to three steps. You can't nest wrappers infinitely and expect coercion to resolve it. The compiler stops after three derefs and gives up.
Coercion only happens when the compiler needs a reference. It triggers in function arguments, method calls, and assignments where the target type is a reference. It does not trigger for moves. If you try to pass a Box<T> to a function expecting T, Deref won't help. You need to move the value out explicitly, usually with *box or Box::into_inner.
Real code: The cost of transparency
In real code, Deref is often used to build smart pointers. Box, Rc, Arc, and Vec all implement Deref. This is why you can pass &vec to a function expecting &[T]. The vector coerces to a slice.
Sometimes you build your own wrapper to add metadata. Here's a string that tracks how many times it's been accessed.
use std::ops::Deref;
use std::cell::Cell;
/// A string that counts how often it's dereferenced.
struct TrackedString {
inner: String,
hits: Cell<u32>,
}
impl TrackedString {
fn new(s: &str) -> Self {
TrackedString {
inner: s.to_string(),
hits: Cell::new(0),
}
}
fn hit_count(&self) -> u32 {
self.hits.get()
}
}
/// Implement Deref to expose the String.
impl Deref for TrackedString {
type Target = String;
fn deref(&self) -> &Self::Target {
// Increment the counter every time we deref.
self.hits.set(self.hits.get() + 1);
&self.inner
}
}
/// A function that only cares about the text.
fn process(text: &str) {
println!("Got: {}", text);
}
fn main() {
let tracked = TrackedString::new("hello");
// Coercion inserts .deref() calls.
// &TrackedString -> &String -> &str.
// Two derefs happen. The counter increments twice.
process(&tracked);
println!("Hits: {}", tracked.hit_count());
}
Run this and you'll see the hit count is 2. The compiler inserted two .deref() calls to bridge the gap from TrackedString to str. Each call ran the body of deref, incrementing the counter.
This reveals a hard rule about Deref. The implementation must be cheap and side-effect free. If deref mutates state, logs, or allocates, you break the mental model of every Rust programmer. Coercion happens silently. Callers don't expect a function call to trigger hidden logic just because they passed a reference.
Keep deref pure. Side effects here break the mental model of every Rust programmer.
Pitfalls and compiler errors
Deref coercion simplifies code, but it introduces traps.
Mutable access requires DerefMut. Implementing Deref only grants immutable access. If you have &mut Wrapper and try to mutate the inner value, the compiler rejects you. It can only coerce to &Inner, not &mut Inner. You'll get E0596 (cannot borrow as mutable) or a "method not found" error because the mutable method requires &mut self. To support mutation, you must also implement DerefMut.
Coercion doesn't help with moves. If a function takes ownership of T, passing &Wrapper won't work. The compiler won't deref to get a value. It only derefs to get references. You'll see E0308 (mismatched types). You need to move the value out explicitly.
Chaining has a limit. The compiler only chains three derefs. If you wrap a value in four layers of Deref types, coercion stops. You'll get a type mismatch error. Flatten your wrappers or deref manually.
Deref can hide type changes. Because coercion happens automatically, the type of an expression can change silently. &box_str might be &String in one context and &str in another. This can make debugging harder. If you're confused about types, check the coercion chain. The compiler is inserting code you didn't write.
Don't hide state behind deref. Keep the implementation trivial.
Deref versus AsRef
A common question is when to use Deref versus AsRef. Both let you convert a type to a reference. They serve different purposes.
Deref is for transparency. It says "this type is a wrapper around another type." It enables coercion. It's structural. Box<T> implements Deref<Target=T> because a box is literally a pointer to T.
AsRef is for conversion. It says "this type can be viewed as T." It's a trait bound, not a coercion mechanism. AsRef doesn't trigger automatically. You call .as_ref() explicitly. It's useful for generic functions that accept multiple input types.
For example, a function that processes text might accept String, &String, &str, or Vec<u8>. You can't use Deref for all of these. Vec<u8> doesn't deref to str. But you can use AsRef<[u8]> or AsRef<str> to write a generic function that handles many types.
Use Deref when you're building a smart pointer or a transparent wrapper. Use AsRef when you're writing a function that needs to accept multiple types and convert them to a common reference.
Deref is for transparency. AsRef is for conversion. Don't mix them up.
When to use Deref
Use Deref when you want a wrapper type to behave transparently like the inner type for read-only access. This lets you pass &Wrapper to functions expecting &Inner without the caller writing .deref() or *.
Use DerefMut when the wrapper needs to support mutation through the reference. Implementing Deref alone only grants immutable access; adding DerefMut allows &mut Wrapper to coerce to &mut Inner.
Use explicit accessor methods when the wrapper has state or invariants that must be checked before access. Deref is too coarse-grained if you need to validate data on every read. Expose specific methods instead.
Reach for AsRef when you need a one-off conversion between types without implying a structural wrapper relationship. AsRef is for "I can be viewed as T", while Deref is for "I am a container of T".
Stick to standard smart pointers like Box and Rc for most use cases. Implementing Deref yourself is rare outside of library code building new abstractions.
Trust the borrow checker. If Deref makes the types confusing, you're probably overusing it.