On this page

Associated Types and GATs

Associated types (type Item) allow each implementation of a trait to have only one Item type, complementing generic parameters (where each implementation can have infinite Ts)—associated types enforce uniqueness, while generic parameters allow diversity. GATs (Generic Associated Types, 1.65+) allow associated types themselves to carry generic parameters, unlocking abstractions that were previously impossible to express, such as traits that return borrowed data from themselves and generic iterators.

Associated Types: One Concrete Type per impl

trait Iterator {
    type Item;                       // Associated type: each implementer specifies a concrete type
    fn next(&mut self) -> Option<Self::Item>;
}

impl Iterator for Counter {
    type Item = u32;
    fn next(&mut self) -> Option<u32> { ... }
}

Why Not Use Generic Parameters?

You certainly could write trait Iterator<T> { fn next(&mut self) -> Option<T>; }. However, this would mean that Counter could simultaneously implement Iterator<i32>, Iterator<String>, and Iterator<f64>—one independent impl for each T. This doesn't make sense for Iterator—a type should only produce one kind of item.

Associated types express a "one-to-one" semantic constraint: at most one impl per type. Generic parameters express a "one-to-many" relationship: a type can have multiple impls for different generic parameters.

This is not just a matter of semantic preference—it affects type inference. When using associated types, Iterator::Item can be uniquely determined from Self. When using generic parameters, the compiler must infer T from the context. Associated types make the compiler's job simpler and the API clearer.

GATs (Generic Associated Types): Associated Types with Lifetimes

Rust 1.65 introduced GATs—associated types can now have their own lifetime or generic parameters:

trait LendingIterator {
    type Item<'a> where Self: 'a;    // Associated type with a lifetime parameter!
    fn next(&mut self) -> Option<Self::Item<'_>>;
}

What problem do GATs solve? The traditional Iterator trait cannot express "returning a reference that borrows from self." You cannot write type Item = &[u8] because references must have a lifetime—and there was nowhere to write a lifetime on an associated type. GATs' type Item<'a> allows associated types to use lifetimes, making Item<'a> = &'a [u8] possible—the returned reference is tied to the lifetime of &self.

This is known as a lending iterator—the iterator "lends" data rather than transferring ownership. The standard library's Iterator is not lending (next returns Option<Self::Item>, it does not borrow &mut self), so you cannot hold a reference to the iterator's internals on the elements returned by the iterator. GATs remove this limitation.

GATs are also the foundation for async traits. The Future type generated by async fn is anonymous and cannot be expressed directly in a trait. GATs allow forms like type Future<'a>: Future<Output = X>—a trait can have a method that returns a Future without the heap allocation overhead of Box<dyn Future>.

impl Trait in Return Position

fn make_iter() -> impl Iterator<Item = i32> {
    (0..10).filter(|x| x % 2 == 0)    // Returns an anonymous type
}

The caller does not know the concrete return type—they only know that "it implements Iterator<Item = i32>." The compiler knows, so it can perform static dispatch and inlining. The difference from Box<dyn Iterator>: impl Trait uses static dispatch (zero overhead, no virtual function calls), but the caller cannot assign functions with different return types to each other—this is the cost of the compiler's abstraction boundary.

References

  • RFC 1598: Generic Associated Types
  • Rust Blog: "GATs are stable!" (2022)
  • Rust Book: Chapter 19.4

Keywords: associated type, GAT, lending iterator, impl Trait, static dispatch, type inference