Associated types and type aliases

In Rust an associated type is a type that a trait names and each implementation chooses. The trait Iterator has one, Item, and so does every trait that produces a value whose type depends on the implementor. The name appears in the trait, and the concrete type appears in the impl.

The difference from a type parameter is subtle and worth learning early: a parameter makes a trait usable several times for one type, while an associated type fixes one implementation per type. This lesson shows both, the alias syntax, and the error for an implementation that forgets the type.

An associated type

trait Producer { type Item; fn produce(&self) -> Self::Item; } says that every producer names what it produces. The signature of produce uses Self::Item, so the caller knows the return type from the implementation.

The program has two producers, one of an integer and one of a string, and prints both values. The trait itself never mentions either type.

trait Producer { type Item; fn produce(&self) -> Self::Item; } struct Number; struct Text; impl Producer for Number { type Item = i32; fn produce(&self) -> Self::Item { 42 } } impl Producer for Text { type Item = String; fn produce(&self) -> Self::Item { String::from("rust") } } fn main() { println!("{}", Number.produce()); println!("{}", Text.produce()); }

rustc --edition 2024 trait.rs
./trait

42
rust

The implementation names the type once, in type Item = i32, and the method signature can then use Self::Item. The lesson on iterators uses exactly this shape.

One implementation per type

A type may implement a trait once, so an associated type can have only one value for that type. This is the guarantee that lets a caller write a bound such as T: Container and know that the item type is fixed.

The program implements a container for a struct of integers and asks for its first value. The method returns a reference to the item, which is why the signature uses Option<&Self::Item>.

trait Container { type Item; fn first(&self) -> Option<&Self::Item>; } struct Numbers { values: Vec<i32>, } impl Container for Numbers { type Item = i32; fn first(&self) -> Option<&Self::Item> { self.values.first() } } fn main() { let numbers = Numbers { values: vec![3, 5, 7] }; println!("{:?}", numbers.first()); }

rustc --edition 2024 generic_assoc.rs
./generic_assoc

Some(3)

Revealing the item type in the signature is what makes a generic function over containers readable: the caller can name T::Item in its own signature. The next section compares that with a type parameter.

Type aliases

A type alias gives a name to an existing type: type Meters = f64;. It is not a new type, so a Meters is an f64 and can be passed anywhere a float is expected. The alias is documentation that the compiler checks.

The program computes a distance with the alias in the signature and prints the result. The arithmetic is float arithmetic, because the alias does not add a wrapper.

type Meters = f64; fn distance(start: Meters, end: Meters) -> Meters { end - start } fn main() { let travelled: Meters = distance(1.5, 4.0); println!("{travelled}"); }

rustc --edition 2024 alias.rs
./alias

2.5

For a distinct type that cannot be mixed with other numbers, the lesson on tuple structs uses a newtype instead. The alias is for a shorter or clearer spelling, not for type safety.

A type parameter instead

A trait may take a type parameter: trait Scale<T>. Then one type can implement it several times with different parameters, and the caller names the one it wants. The two implementations below scale by a float and by an integer.

The program calls both on the same value and prints the two results. The call resolves the parameter from the argument type, so the two methods coexist.

trait Scale<T> { fn scale(&self, factor: T) -> f64; } struct Value(f64); impl Scale<f64> for Value { fn scale(&self, factor: f64) -> f64 { self.0 * factor } } impl Scale<i32> for Value { fn scale(&self, factor: i32) -> f64 { self.0 * f64::from(factor) } } fn main() { let value = Value(2.5); println!("{} {}", value.scale(2.0), value.scale(3)); }

rustc --edition 2024 repeated_impl.rs
./repeated_impl

5 7.5

The rule of thumb: an associated type when each implementor produces one kind of thing, a parameter when the same implementor supports several. Iterator has an associated type because a collection has one item type.

The item left out

An implementation must name every associated type, just as it must write every method without a default. Leaving one out is E0046, and the message names the item and the trait.

The program implements the producer without type Item. The compiler reports the missing item at the impl block, and the fix is one line.

trait Producer { type Item; fn produce(&self) -> Self::Item; } struct Number; impl Producer for Number { fn produce(&self) -> i32 { 42 } } fn main() { println!("{}", Number.produce()); }

rustc --edition 2024 missing_item_err.rs

error[E0046]: not all trait items implemented, missing: `Item`
--> missing_item_err.rs:9:1
|
2 | type Item;
| --------- `Item` from trait
...
9 | impl Producer for Number {
| ^^^^^^^^^^^^^^^^^^^^^^^^ missing `Item` in implementation

error: aborting due to 1 previous error

For more information about this error, try `rustc --explain E0046`.

An associated type may have a default in the trait, which makes it optional in an implementation. The standard library uses that rarely, because a wrong default is worse than a missing line.

Exercises

  1. Add a third producer that yields a bool and print its value.
  2. Write a function that takes &impl Producer and prints the produced value. Which type does the item have inside the function?
  3. Replace the Meters alias with a tuple struct and see which lines stop compiling.

Questions

A new set of questions in a new order every time. Choose one answer for each, then check.

  1. What is an associated type?

    Answer

    A type named in a trait and chosen by each implementation

    The trait says that the type exists, and the impl says which type it is.

  2. How many times may a type implement a trait with an associated type?

    Answer

    Once

    The implementation fixes the associated type, so a second one would conflict.

  3. What is a type alias?

    Answer

    Another name for an existing type

    The alias adds a spelling, not a type: the value is still the original type.

  4. When is a type parameter better than an associated type?

    Answer

    When one type must implement the trait for several different types

    A parameter allows several implementations on one type; an associated type fixes one.

  5. What does this program print? trait Producer { type Item; fn produce(&self) -> Self::Item; } struct Number; impl Producer for Number { type Item = i32; fn produce(&self) -> Self::Item { 42 } } fn main() { println!("{}", Number.produce()); }

    Answer

    42

    The implementation fixes Item as i32, and the method returns the number.

  6. What does this program print? trait Container { type Item; fn first(&self) -> Option<&Self::Item>; } struct Numbers { values: Vec<i32>, } impl Container for Numbers { type Item = i32; fn first(&self) -> Option<&Self::Item> { self.values.first() } } fn main() { let numbers = Numbers { values: vec![3, 5, 7] }; println!("{:?}", numbers.first()); }

    Answer

    Some(3)

    The vector has a first element, and the Option prints it in the debug form.

  7. What does this program print? type Meters = f64; fn distance(start: Meters, end: Meters) -> Meters { end - start } fn main() { let travelled: Meters = distance(1.5, 4.0); println!("{travelled}"); }

    Answer

    2.5

    The alias is a float, and four minus one and a half is two and a half.

  8. What does this program print? trait Scale<T> { fn scale(&self, factor: T) -> f64; } struct Value(f64); impl Scale<f64> for Value { fn scale(&self, factor: f64) -> f64 { self.0 * factor } } impl Scale<i32> for Value { fn scale(&self, factor: i32) -> f64 { self.0 * f64::from(factor) } } fn main() { let value = Value(2.5); println!("{} {}", value.scale(2.0), value.scale(3)); }

    Answer

    5 7.5

    The parameter type selects the implementation, and the two products are five and seven and a half.

  9. What is the first line of the compiler output for this program? trait Producer { type Item; fn produce(&self) -> Self::Item; } struct Number; impl Producer for Number { fn produce(&self) -> i32 { 42 } } fn main() { println!("{}", Number.produce()); }

    Answer

    error[E0046]: not all trait items implemented, missing: Item

    The impl writes the method but not the associated type, and the message names it.

  10. What does this program print? trait Name { type Output; fn make(&self) -> Self::Output; } struct A; struct B; impl Name for A { type Output = &'static str; fn make(&self) -> Self::Output { "A" } } impl Name for B { type Output = i32; fn make(&self) -> Self::Output { 2 } } fn main() { println!("{} {}", A.make(), B.make()); }

    Answer

    A 2

    Each implementation chooses its own output type, and the two calls print their values.

The code on this page was built with rustc 1.96.1 and run on Alpine Linux 3.24.3 (x86_64) in a Docker container.

Article author: Arthur Isaev