Deref Trait and Deref Coercion

Learn how to implement the Deref trait to make custom types behave like references and how Rust’s deref coercion automatically matches types in function arguments.

When you write *pointer in Rust, you are asking the compiler to follow the pointer and give you the value it points to. For plain references, this “dereference” behavior is built into the language. But Rust’s smart pointers, such as Box<T>, Rc<T>, and Arc<T>, can also be dereferenced with * — even though they are not raw references. The mechanism that enables this is the Deref trait, and the compiler feature that lets these types flow into functions that expect references is called deref coercion.

These two pieces work together to make smart pointers feel almost invisible. You can write code that accepts a &str and call it with a &String, a &Box<String>, or even a &Rc<String> without writing explicit conversions. That level of ergonomics is rare in systems languages, and it comes from careful design of Deref and deref coercion.

The following sections walk through how to bring this behavior to your own types and how the compiler uses it when resolving function calls.

Implementing Deref for Custom Types

To understand what the Deref trait provides, start with a plain reference. A reference is a pointer to a value stored elsewhere. When you have let y = &x, the variable y is not the integer 5; it is the address of x. If you want the actual integer, you use the dereference operator *y to follow the pointer to the data.

fn main() {
    let x = 5;
    let y = &x;
    assert_eq!(5, x);
    assert_eq!(5, *y);
}

Without *, comparing y to 5 would fail because y is a &i32 and 5 is an i32 — different types. The dereference operator resolves that mismatch.

A Box<T> behaves exactly the same way, even though it allocates the value on the heap.

fn main() {
    let x = 5;
    let y = Box::new(x);
    assert_eq!(5, x);
    assert_eq!(5, *y);
}

The Box stores the integer on the heap, but *y still gives you the integer back. That works because Box<T> implements the Deref trait. If you try to build your own wrapper and skip that trait, the compiler stops you.

Consider a simple tuple struct that holds a value:

struct MyBox<T>(T);
impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}

If you attempt to use * on a MyBox<i32>, the compiler produces:

error[E0614]: type `MyBox<{integer}>` cannot be dereferenced

The type has no dereference logic yet. The fix is to implement Deref. The trait requires an associated type Target and a method deref that borrows self and returns a reference to that target type.

use std::ops::Deref;
impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}

The associated type Target = T tells the compiler what type a dereference should produce — in this case, the inner T. The deref body returns &self.0, a reference to the first (and only) field of the tuple struct.

With this implementation, *y compiles. Behind the scenes, the compiler expands *y into *(y.deref()). It calls deref to get a reference to the inner value and then applies the built-in dereference to that reference. This transformation is applied exactly once — it does not recurse — so the result is the inner i32.

Returning a Reference Is Required:

The deref method must return a reference, not the value itself. If it returned T directly, the inner value would be moved out of self, and the smart pointer would lose ownership of its data. Since deref takes &self, moving out is not allowed — returning a reference preserves the ownership structure and keeps the wrapper intact.

The pattern of returning a reference is why Deref works for immutable access. For mutable dereferencing, there is a separate trait DerefMut that returns &mut Self::Target. That trait is beyond the scope of this discussion, but the principle is identical: borrow, don’t move.

A common beginner mistake is to think of Deref as a general-purpose conversion mechanism. It is not. The standard library documentation states that Deref should only be implemented when your type transparently behaves like a reference to its target, the deref operation is cheap, and users will not be surprised by automatic coercion. If your type has methods that clash with methods on the target type, or if deref can fail, Deref is the wrong tool — consider AsRef or Borrow instead.

Method Resolution Can Be Surprising:

When a type implements Deref, any method call on it will first look for methods on the wrapper type itself. If none match, the compiler then searches methods on the target type. This means if both the wrapper and the target have a method with the same name, the wrapper’s method wins — even if the signature differs. This is not overload resolution; it is simple priority. If you add methods to a type that implements Deref, be careful that they don’t silently shadow methods from the inner type.

Once you’ve implemented Deref, your custom type works with the * operator just like a reference. The real power, however, becomes visible when you pass it to functions — which is where deref coercion takes over.

Deref Coercion with Functions and Methods

Deref coercion is the compiler’s ability to automatically convert a reference to a type that implements Deref into a reference to its target. This happens during function and method calls when the argument type does not match the parameter type but a chain of deref calls can bridge the gap. The entire process resolves at compile time with zero runtime cost.

A classic example involves String and &str. String implements Deref<Target = str>, so a &String can be coerced to a &str automatically.

fn greet(name: &str) {
    println!("Hello, {name}!");
}
fn main() {
    let name = String::from("Rust");
    greet(&name); // &String coerced to &str
}

Without deref coercion, you would need to write greet(&name[..]) or greet(name.as_str()) every time. The compiler inserts the call to deref for you, turning &String into &str.

The coercion can chain through multiple levels. If you have a MyBox<String> (where MyBox<T> implements Deref<Target = T>), passing &my_box to a function expecting &str works because the compiler applies MyBox::deref to get a &String and then String::deref to get a &str.

use std::ops::Deref;
struct MyBox<T>(T);
impl<T> MyBox<T> {
    fn new(x: T) -> MyBox<T> {
        MyBox(x)
    }
}
impl<T> Deref for MyBox<T> {
    type Target = T;
    fn deref(&self) -> &Self::Target {
        &self.0
    }
}
fn greet(name: &str) {
    println!("Hello, {name}!");
}
fn main() {
    let my_box = MyBox::new(String::from("Rust"));
    greet(&my_box); // &MyBox<String> -> &String -> &str
}

The compiler performs as many deref steps as needed until the types match. This chaining happens silently, but it follows strict rules: only references to types implementing Deref are coerced, and the compiler will never coerce through more than one level of reference itself (that is, &&T does not become &U unless Deref is implemented for &T, which is rare).

Deref Coercion Works at Compile Time:

Because all deref calls are resolved statically, there is no runtime dispatch or performance penalty. The compiler knows the concrete types and inserts the exact sequence of dereferences directly into the generated code. This makes deref coercion a purely ergonomic feature — it simplifies code without affecting what the CPU actually executes.

Deref coercion also applies when you call methods. If a type T implements Deref<Target = U>, all methods of U that take &self are available on T as if they were defined there. This is why you can call str methods like .len() or .chars() directly on a String, and why Box<T> delegates method calls to T. The compiler automatically inserts the dereference, so you never need to write (*box).method().

Seamless Interop:

If you’ve implemented Deref correctly on a wrapper type and passed it to a function that expects a reference to the inner type, and everything compiled and ran without panics, deref coercion is working as intended. This seamless interoperability is the hallmark of well-designed smart pointers in Rust.

The choice of where to apply deref coercion is deliberately narrow. It only triggers when the compiler sees a type mismatch between a &T argument and a &U parameter (or when resolving method calls). It does not coerce owned values, mutable references (without DerefMut), or raw pointers. This restraint keeps the feature predictable: you know coercion is at play only in function arguments and method resolution.

A common misconception is that deref coercion makes types “the same.” It does not. A &Box<String> is still a &Box<String>; the compiler merely inserts a reference to the inner String when the context demands a &str. The original type is unchanged, and ownership remains exactly where it was.

Understanding when and how deref coercion fires helps you avoid two practical pitfalls:

  1. Accidental infinite coercion. The compiler will stop coercing once the types match. You cannot create an infinite chain because each step moves from &T to &<T as Deref>::Target. The chain length is bounded by the concrete type hierarchy, which is always finite.
  2. Over‑reliance on Deref for inheritance‑like patterns. Because method calls first check the wrapper type’s own methods, using Deref to simulate “base class” delegation can break silently if you later add a method with the same name. Explicit delegation through AsRef or a dedicated method is safer when the relationship is not pointer‑like.

Do Not Use Deref for General Conversion:

Implement Deref only when your type genuinely acts as a transparent pointer to its inner value. If you are wrapping a type to add behavior or restrict functionality, and deref coercion would expose all the inner type’s methods unexpectedly, stop. The Deref trait is a promise that your type can be treated as a reference to the target in almost every context. Breaking that promise leads to confusing API designs.

The combination of Deref and deref coercion eliminates boilerplate that plagues many systems languages. You write a function once against a reference type, and it works with any smart pointer that Derefs to that reference. This property is what allows standard library types like Box, Rc, Arc, Ref, and MutexGuard to integrate with existing &-based APIs without any special glue code.


Rust’s Deref trait and deref coercion are not independent features — they form a single contract. Deref defines what it means to “look through” a type to get at the value inside. Deref coercion is the compiler automation that applies that definition wherever a function signature asks for it.