Skip to content
C Codeloom
Rust

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.

·5 min read · By Codeloom
Intermediate 9 min read

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.

Deref coercion vs AsRef conversions

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.