Traits in Rust are like interfaces in other languages. They define shared behavior that types can implement. You can use built-in traits like Debug, or define your own. Traits let you write code that works across different types, as long as they implement the expected behavior.
When I began working with generics in Rust, I quickly ran into the need to define what a type can do, not just what type it is. That is where traits came in. Traits are how Rust expresses shared capabilities like formatting, comparison, or custom actions you define yourself.
In this post, I will show you how traits work, how to implement them, how to use derive for built-in ones like Clone or Debug, and how to create and use your own.
What Is a Trait?
A trait is a collection of methods that a type can implement. If a type implements a trait, then you can call the methods that trait defines on that type.
You can think of traits as contracts. If a type agrees to the contract, it must provide the behavior promised by the trait.
Rust has many built-in traits, like:
- Debug – used for printing with println!(“{:?}”, value)
- Clone – allows copying values with .clone()
- PartialEq – enables == comparisons
Using Derive to Implement Common Traits
Rust provides a derive macro that automatically implements some common traits for your types. This is useful when you want basic functionality without writing code by hand.
Here is a simple example using a struct:
#[derive(Debug, Clone, PartialEq)]
struct User {
username: String,
active: bool,
}
This tells Rust to automatically generate implementations for:
- Debug – so we can print with {:?}
- Clone – so we can use .clone()
- PartialEq – so we can compare users with ==
You can now do things like:
let user1 = User {
username: String::from("alice"),
active: true,
};
let user2 = user1.clone();
println!("{:?}", user2);
println!("Are equal? {}", user1 == user2);
These traits are used all over the Rust standard library, so adding derive makes your types easier to use with built-in tools.
We have already seen traits like Copy, Clone, and Debug in earlier posts like Primitive Data Types in Rust and Cloning Data in Rust. This builds on that idea.
Defining Your Own Traits
You can define your own traits to describe behavior that you want to share across types.
Here is a trait that defines a method called describe:
trait Describe {
fn describe(&self) -> String;
}
This means that any type that implements Describe must provide a method that returns a String description.
Implementing a Trait for a Type
Let us implement Describe for a struct:
struct Product {
name: String,
price: f64,
}
impl Describe for Product {
fn describe(&self) -> String {
format!("{} costs ${}", self.name, self.price)
}
}
Now you can do:
let p = Product {
name: String::from("Shoes"),
price: 49.99,
};
println!("{}", p.describe());
You can implement the same trait for multiple types:
struct User {
username: String,
email: String,
}
impl Describe for User {
fn describe(&self) -> String {
format!("{} ({})", self.username, self.email)
}
}
Both Product and User now share a common behavior, even though they are unrelated structs.
Calling Trait Methods on Generic Types
This becomes even more powerful when combined with generics. You can write a function that works for any type that implements a specific trait.
fn print_description<T: Describe>(item: T) {
println!("{}", item.describe());
}
Now you can pass a User, a Product, or anything else that implements Describe.
We will explore this concept further in the next post on Trait Bounds and Constraints.
Traits vs Interfaces
If you are coming from a JavaScript or TypeScript background, think of traits as interfaces with default methods.
However, traits are more powerful:
- You can implement traits for types you did not define (as long as one of the trait or type is local)
- You can provide default implementations inside the trait
trait Greet {
fn say_hello(&self) {
println!("Hello!");
}
}
If a type implements Greet without overriding say_hello(), it gets the default version.
Limitations and Orphan Rule
You cannot implement a foreign trait for a foreign type. For example, you cannot implement ToString for i32 because:
- You did not define the ToString trait (it is from the standard library)
- You did not define the i32 type
This is known as the orphan rule and it protects you from accidentally conflicting with other crates.
Common Built-in Traits to Know
Trait | Purpose |
---|---|
Debug | Enables {:?} formatting |
Clone | Allows deep copying |
Copy | Allows bitwise copy (cheap types) |
PartialEq | Enables comparison with == |
Eq | Used with PartialEq for full equality |
Default | Provides a default value |
ToString | Converts to a String using .to_string() |
From / Into | Convert between types |
Summary
Traits are a core part of Rust’s design. They let you describe what a type can do, not just what it is. You use them to share behavior, write generic code, and unlock powerful abstractions. Whether you are using derive for built-in traits or writing your own, traits help you write more expressive and reusable Rust code.
Rust Intermediate Concepts
- Generic Types in Functions and Structs
- Implementing Traits for Custom Behavior in Rust
- Trait Bounds and Constraints in Rust
- Lifetimes in Rust
- Using Closures (Anonymous Functions) in Rust
- The Iterator Trait and .next() in Rust
- Higher Order Iterator Methods: map, filter, and fold in Rust
- Using impl Trait for Simplicity in Rust
- Advanced Collections: HashSet and BTreeMap in Rust
- Custom Error Types and the Error Trait in Rust
- Option and Result Combinators in Rust
- Writing Unit Tests in Rust
- Integration Testing in Rust
- Logging in Rust with the log Crate
- Cargo Tips and Tricks for Rust Projects
- CLI Argument Parsing with Clap in Rust
- File I/O and File System Operations in Rust
- Running External Commands in Rust
- Make HTTP Requests in Rust with Reqwest
- JSON Serialization and Deserialization in Rust with Serde
- Building a Weather CLI in Rust
- Date and Time Handling in Rust with Chrono
- Using Regular Expressions in Rust with the regex Crate
- Memory Management in Rust
- Understanding Borrow Checker Errors in Rust
- Interacting with Databases in Rust
- Building a Todo List CLI in Rust with SQLite
- Concurrency in Rust