Nugget 7

Home

Adding Methods to Types You Don't Own

You wish "hello@example.com" had an .is_email() method, but &str is a built-in type. Extension traits let you add methods to any type — even ones from other crates.

The Problem

You can't add methods to &str or String directly — they belong to the standard library:

// You want:
"hello@example.com".is_email();  // ❌ no such method on &str

// So you write a free function instead:
fn is_email(s: &str) -> bool {
    s.contains('@')
}

// But call sites are less readable:
if is_email(&user_input) { /* ... */ }

Free functions work, but method syntax (user_input.is_email()) reads more naturally in many cases.

The Solution: An Extension Trait

Define a trait with the method you want, then implement it for the target type. When you bring the trait into scope with use, the method appears on the type:

// 1. Define a trait
trait EmailCheck {
    fn is_email(&self) -> bool;
}

// 2. Implement it for &str
impl EmailCheck for str {
    fn is_email(&self) -> bool {
        self.contains('@') && self.contains('.')
    }
}

// 3. Use it — just bring the trait into scope
fn main() {
    let s = "hello@example.com";
    println!("{}", s.is_email());  // ✅ true

    let bad = "not-an-email";
    println!("{}", bad.is_email()); // ✅ false
}

📐 Note: str vs &str

We implement for str (the unsized type), not &str (a reference). &self in the trait becomes a reference to str, which is &str. This applies to any method taking &self.

More Examples

Extension traits are especially useful for common utility methods:

// Extending Vec with a shorthand
trait VecUtils<T> {
    fn first_or<'a>(&'a self, default: &'a T) -> &'a T;
}

impl<T> VecUtils<T> for Vec<T> {
    fn first_or<'a>(&'a self, default: &'a T) -> &'a T {
        self.first().unwrap_or(default)
    }
}

fn main() {
    let v: Vec<i32> = vec![];
    let n = v.first_or(&42);
    println!("{}", n);  // 42
}
// Extending Option for convenience
trait OptionStringExt {
    fn unwrap_or_empty(self) -> String;
}

impl OptionStringExt for Option<String> {
    fn unwrap_or_empty(self) -> String {
        self.unwrap_or_default()
    }
}

fn main() {
    let name: Option<String> = None;
    println!("Hello, {}!", name.unwrap_or_empty());  // Hello, !
}

Real World: itertools

The most famous extension trait in the Rust ecosystem is itertools::Itertools. It adds dozens of extra methods to all iterators:

use itertools::Itertools;  // bring extension trait into scope

fn main() {
    let chars = "hello"
        .chars()
        .unique()          // ✅ not on standard Iterator
        .sorted()          // ✅ not on standard Iterator
        .join(", ");       // ✅ not on standard Iterator

    println!("{}", chars); // e, h, l, o
}

Itertools is a trait. You use it, and suddenly every iterator in your code gains those methods.

One Rule: The Orphan Check

Rust has an "orphan rule": you can implement a trait for a type only if you own either the trait or the type (or both). This prevents conflicting implementations across crates:

// ✅ You own both the trait and the type
impl EmailCheck for str { /* ... */ }

// ✅ You own the type, implementing a std trait
impl From<MyType> for String { /* ... */ }

// ❌ Neither is yours — blocked by orphan rule
impl Display for Vec<String> { /* ... */ }
// (Display is std, Vec is std — won't compile)

💡 The newtype pattern

To work around the orphan rule, wrap the foreign type in a tuple struct: struct MyVec(Vec<String>). Now you "own" the outer type and can implement any trait on it.

Key Takeaways