Skip to content
Pre-Release: Fe is under active development. This documentation covers the upcoming release. Follow progress on GitHub

Trait Bounds

Trait bounds specify what capabilities a generic type must have. They let you write generic code that uses specific methods or behaviors, with the compiler ensuring only valid types are used.

Add a bound with : Trait after the type parameter:

fn process<T: Hashable>(value: T) -> u256 {
value.hash() // Safe: T must implement Hashable
}

Without the bound, you couldn’t call hash():

fn process<T>(value: T) -> u256 {
value.hash() // Error: T might not have hash()
}

Bounds enable the compiler to:

  1. Verify method availability: Ensure methods you call exist
  2. Catch errors early: Fail at call site, not deep in implementation
  3. Document requirements: Show what types are expected
trait Printable {
fn to_string(self) -> String
}
// Clear: this function needs Printable types
fn log<T: Printable>(value: T) {
let s = value.to_string()
// ... log the string
}
struct Secret {
data: u256,
}
// Error at call site: Secret doesn't implement Printable
log(Secret { data: 42 }) // Compile error

Require multiple traits with +:

trait Hashable {
fn hash(self) -> u256
}
trait Printable {
fn to_string(self) -> String
}
fn describe<T: Hashable + Printable>(value: T) -> String {
let _hash = value.hash()
value.to_string()
}

The type must implement all specified traits:

struct Token {
id: u256,
}
impl Hashable for Token {
fn hash(self) -> u256 {
self.id
}
}
impl Printable for Token {
fn to_string(self) -> String {
"Token"
}
}
describe(Token { id: 1 }) // Works: Token implements both

Each type parameter can have its own bounds:

fn combine<A: Hashable, B: Printable>(a: A, b: B) -> String<256> {
let hash = a.hash()
b.to_string()
}
fn transform<T: Readable, U: Writable>(input: T, mut output: U) {
let value = input.read()
output.write(value)
}

Generic structs can have bounds:

struct Cache<T: Hashable> {
item: T,
hash: u256,
}
impl<T: Hashable> Cache<T> {
fn new(item: T) -> Cache<T> {
let hash = item.hash()
Cache { item, hash }
}
}

Impl blocks can add their own bounds:

struct Wrapper<T> {
value: T,
}
// Basic methods, no bounds needed
impl<T> Wrapper<T> {
fn get(self) -> T {
self.value
}
}
// Methods that need Printable
impl<T: Printable> Wrapper<T> {
fn print(self) -> String {
self.value.to_string()
}
}
// Methods that need both traits
impl<T: Hashable + Printable> Wrapper<T> {
fn describe(self) -> String {
let _hash = self.value.hash()
self.value.to_string()
}
}

This means:

  • All Wrapper<T> have get()
  • Only Wrapper<T: Printable> have print()
  • Only Wrapper<T: Hashable + Printable> have describe()
trait Comparable {
fn less_than(self, other: Self) -> bool
fn equals(self, other: Self) -> bool
}
fn min<T: Comparable>(a: T, b: T) -> T {
if a.less_than(b) { a } else { b }
}
fn find<T: Comparable>(items: Array<T>, target: T) -> bool {
for item in items {
if item.equals(target) {
return true
}
}
false
}
trait Default {
fn default() -> Self
}
fn or_default<T: Default>(value: Option<T>) -> T {
match value {
Option::Some(v) => v,
Option::None => T::default(),
}
}
trait Clone {
fn clone(self) -> Self
}
fn duplicate<T: Clone>(value: T) -> (T, T) {
(value.clone(), value)
}

Bounds and effects work together:

trait Storable {
fn key(self) -> u256
}
fn save<T: Storable>(item: T) uses (mut storage: Storage) {
let key = item.key()
storage.set(key, item)
}
fn load<T: Storable + Default>(key: u256) -> T uses (storage: Storage) {
storage.get(key)
}

When bounds aren’t satisfied, you get clear errors:

trait Hashable {
fn hash(self) -> u256
}
fn process<T: Hashable>(value: T) -> u256 {
value.hash()
}
struct NoHash {
data: u256,
}
process(NoHash { data: 1 })
// Error: NoHash does not implement Hashable

The error points to the call site, making it easy to understand what’s missing.

SyntaxDescription
T: TraitSingle trait bound
T: A + BMultiple trait bounds
<T: Trait>Bound in function signature
struct Foo<T: Trait>Bound in struct definition
impl<T: Trait>Bound in impl block