Trait bounds in Rust let you write generic code that only works for types with specific capabilities. You use T: Trait or where clauses to constrain what types are allowed. This helps you keep your code flexible but still type-safe.
After I started using generics, I found myself needing to limit which types could be passed into a function. I wanted to say, “This function works for any type… but only if it knows how to display itself or be cloned.” That is exactly what trait bounds are for. They help you strike the balance between flexibility and control.
In this post, I will show you how to use trait bounds in function signatures, structs, and generic implementations. We will also look at how this enables static polymorphism, where different types share behavior at compile time.
Why Trait Bounds Matter
Generic functions are flexible, but that flexibility comes with a cost: Rust cannot assume what your type can do unless you tell it.
If you want to call methods from a trait like Display, Clone, or one you define, the compiler must know your type supports those methods. Trait bounds let you tell Rust exactly that.
This builds directly on the previous post, Implementing Traits for Custom Behavior.
Trait Bounds in Function Signatures
The simplest form of a trait bound looks like this:
fn print_twice<T: std::fmt::Display>(value: T) {
println!("{}", value);
println!("{}", value);
}
This function works for any type T, as long as T implements the Display trait.
Without the bound, Rust would not know that value can be formatted with {}.
You can call this with String, i32, or any custom type that implements Display.
print_twice(42);
print_twice("hello");
Multiple Trait Bounds
You can require more than one trait using a plus sign:
fn print_and_clone<T: std::fmt::Display + Clone>(value: T) {
let copy = value.clone();
println!("Original: {}", value);
println!("Clone: {}", copy);
}
This tells Rust that T must implement both Display and Clone.
Using where Clauses
If your trait bounds get long, you can move them to a where clause:
fn print_and_clone<T>(value: T)
where
T: std::fmt::Display + Clone,
{
let copy = value.clone();
println!("Original: {}", value);
println!("Clone: {}", copy);
}
This is easier to read, especially for functions with multiple type parameters.
Trait Bounds in Structs
You can also apply trait bounds when defining generic structs:
struct Wrapper<T: std::fmt::Display> {
value: T,
}
This tells Rust that T must implement Display, so you can write methods that use println! safely.
Or use impl blocks with trait bounds:
impl<T: std::fmt::Display> Wrapper<T> {
fn show(&self) {
println!("{}", self.value);
}
}
Trait Bounds in Enums
You can even add bounds when implementing traits for enums:
enum Either<T, U> {
Left(T),
Right(U),
}
impl<T: std::fmt::Display, U: std::fmt::Display> Either<T, U> {
fn describe(&self) {
match self {
Either::Left(l) => println!("Left: {}", l),
Either::Right(r) => println!("Right: {}", r),
}
}
}
Trait bounds keep the behavior safe while still supporting multiple types.
Returning Types with Trait Bounds
You can return generic types with trait bounds, or even use impl Trait to make it simpler.
This function returns something that implements the Iterator trait:
fn make_iterator() -> impl Iterator<Item = i32> {
vec![1, 2, 3].into_iter()
}
We will go deeper into impl Trait in a later post.
Trait Bounds for Your Own Traits
If you define your own trait, you can use bounds in the same way:
trait Summarize {
fn summary(&self) -> String;
}
fn print_summary<T: Summarize>(item: T) {
println!("{}", item.summary());
}
Any type that implements Summarize can be passed in.
This is a common pattern in reusable libraries and APIs.
Trait Bounds and Static Polymorphism
When you use trait bounds, Rust resolves the exact implementation at compile time. This is called static dispatch or monomorphization.
It means your code is:
- Fast: no dynamic lookup or overhead
- Type-safe: all errors caught at compile time
- Flexible: works across many types
This is different from dynamic polymorphism in languages like JavaScript, where behavior is resolved at runtime.
Summary
Trait bounds let you define what a type must be able to do, not just what type it is. You can use them in function signatures, structs, enums, and more. They keep your generic code safe, powerful, and expressive. As you continue building with Rust, you will use trait bounds often to write reusable, readable code.