---
title: 'Associated Types'
source: 'https://academia.sh/en/courses/rust-abstraction/associated-types'
course: 'Traits, Generics and Error Handling'
language: en
updated: '2026-09-15T04:58:42+00:00'
license: 'CC BY-SA 4.0'
objectives:
    - 'Defines the associated type syntax and distinguishes it from the same contract written with a generic parameter'
    - "Shows that the two forms' run counts come out the same, the difference being in the information the caller has to write"
    - 'Compares and explains that an associated type is a constraint allowing one implementation per type, while a generic parameter allows no such thing'
---

# Associated Types

A trait can carry a type within itself without taking a type parameter — an associated type; when the same contract is written with an associated type and with a generic parameter, the run count comes out exactly the same, the difference is in what the caller has to write in the signature, and an associated type carries a constraint that allows only one implementation per type.

The previous lesson showed how a trait bound constrains a type parameter **from the outside**:
the `T: Label` syntax said which types the caller could pass in. A trait can sometimes do the
opposite too — carry a type **within** the contract itself, a type decided not by the caller
but by the implementer. The kernel that ran in the course's first lesson already had an example
of this: the `Add` trait's implementation carried the line `type Output = Length;` — that line
in the operator-overloading section is exactly this lesson's subject, the **associated type**.
That lesson passed over it without introducing it; this lesson comes back to it.

A trait bound answered the caller's question of "which types can I pass to this function." An
associated type answers a different question: "a type implementing this contract carries which
**other** type alongside itself." A trait can declare not just method signatures but also a
helper type those methods work with — this helper type can differ per implementation, but is
fixed for any single implementation. This means a contract can define not just "what will be
done" but also "which type it will work with."

Neither of the two bodies measured in this lesson leaves the course's three classes — both fall
into just one: **every time**. Neither the associated type nor the generic parameter causes a
body to be deferred or skipped — both only shape the body's **signature**, not whether it runs
when called. This is the same observation as the previous two lessons: the class is determined
not by how the body is named or where it is bound, but by what happens when it is called.

## What Is an Associated Type

Writing `type Name;` inside a trait declaration means "every type implementing this contract
must also declare a `Name` type within itself." During implementation, this slot is filled by a
`type Name = ConcreteType;` line. The two traits below define the same job with two different
notations:

```rust
// times.rs — slice: the record/times ledger, the associated-type and generic-parameter contracts.

use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

pub type Ledger = Rc<RefCell<BTreeMap<String, u32>>>;

pub fn record(ledger: &Ledger, name: &str) {
    *ledger.borrow_mut().entry(name.to_string()).or_insert(0) += 1;
}

pub fn times(ledger: &Ledger, name: &str) -> u32 {
    *ledger.borrow().get(name).unwrap_or(&0)
}

// Contract carries a type within itself: Product is decided by the implementer.
pub trait Contract {
    type Product;
    fn produce(&self, ledger: &Ledger) -> Self::Product;
}

// The same idea, with a generic parameter: C is a type the caller supplies from outside.
pub trait GenericContract<C> {
    fn produce_generic(&self, ledger: &Ledger) -> C;
}

pub struct Container {
    pub value: i32,
}

impl Contract for Container {
    type Product = i32;
    fn produce(&self, ledger: &Ledger) -> i32 {
        record(ledger, "associated-type-body");
        self.value
    }
}

impl GenericContract<i32> for Container {
    fn produce_generic(&self, ledger: &Ledger) -> i32 {
        record(ledger, "generic-parameter-body");
        self.value
    }
}
```

The `Self::` prefix in the `Self::Product` syntax is not arbitrary: when the trait declaration
is written, it is not yet known which type will implement this contract, so `Product` cannot be
written as a bare name — it has to be written as `Self::Product`, meaning "the implementing
type's own `Product`." Inside the `impl` block this ambiguity disappears — since `Self` now
corresponds to `Container`, the line `type Product = i32;` can write `i32` directly, with no
need for the `Self::` prefix. The same name (`Product`) plays two different roles: a
**placeholder** in the trait declaration, a **decision** inside the implementation.

In `Contract`, `Product` is **part of** the contract: the return type of the `produce` method
is written as `Self::Product`, not a concrete type name. When `Container` implements this
contract, it declares once, at the top of the `impl` block, that `Product` is `i32`; the rest
of the block inherits this decision. In `GenericContract<C>`, the same information is instead
**inside the trait's name**: when `impl GenericContract<i32> for Container` is written, `C`
resolves to `i32` there, but because this information is part of the trait name, it can be
repeated more than once, with different `C` values — the constraint side of this difference is
looked at below.

## Same Count, Different Syntax

Calling both notations the same number of times makes the run count directly comparable.

```rust
// associated-type.rs — the same job in two notations: associated type and generic parameter.
mod times;
use times::{times, Ledger, Container, Contract, GenericContract};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let container = Container { value: 9 };

    // The caller never writes Product in the associated-type form - the compiler already knows it.
    let _ = container.produce(&ledger);
    let _ = container.produce(&ledger);
    let _ = container.produce(&ledger);

    // The same number of calls, the same body, with the generic parameter too.
    let _ = GenericContract::<i32>::produce_generic(&container, &ledger);
    let _ = GenericContract::<i32>::produce_generic(&container, &ledger);
    let _ = GenericContract::<i32>::produce_generic(&container, &ledger);

    let mut bodies: BTreeMap<&str, u32> = BTreeMap::new();
    bodies.insert("associated-type-body", times(&ledger, "associated-type-body"));
    bodies.insert("generic-parameter-body", times(&ledger, "generic-parameter-body"));

    println!("{:<24}{}", "body", "how many times");
    for (name, n) in &bodies {
        println!("{:<24}{}", name, n);
    }
}
```

```
body                    how many times
associated-type-body    3
generic-parameter-body  3
```

**TG5.** Both ran three times — two rows in the **every time** class, the exact same number.
The difference does not show up in the run ledger, it shows up in the call line itself:
`container.produce(&ledger)` never says what `Product` is, because the compiler already knows
it from `Container`'s `Contract` implementation. `GenericContract::<i32>::produce_generic(&container,
&ledger)`, on the other hand, carries `i32` explicitly — because `Container` could have more
than one implementation of `GenericContract` (shown below), stating which one is meant becomes
the caller's job.

The Interfaces, Errors and Generics course wrote Go's generics from the **binding** axis; what's
measured here is not the question that course asked, it is the two notations' **body count** —
and the count comes out exactly equal. This shows that an associated type and a generic
parameter carry no different cost at run time; the difference between them is entirely in the
source's **readability** and in the information the caller has to **write**.

## An Associated Type Is Per Type

The constraint applies not to the trait **as a whole**, but separately to each
**implementation**. `Container`'s `Product` is fixed to `i32` once, but another type can
implement the same `Contract` with its own `Product` — what's being constrained is not "this
trait must use one `Product` everywhere," it is "this type must have chosen one `Product` for
this trait."

```rust
// two-types.rs — an associated type is fixed per implementation, not per trait.
mod times;
use times::{times, Ledger, Container, Contract, GenericContract};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

struct Labeled {
    name: String,
}

impl Contract for Labeled {
    type Product = String;
    fn produce(&self, ledger: &Ledger) -> String {
        times::record(ledger, "labeled-body");
        self.name.clone()
    }
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let container = Container { value: 5 };
    let labeled = Labeled { name: String::from("title") };

    let numeric: i32 = container.produce(&ledger);
    let textual: String = labeled.produce(&ledger);
    let _ = GenericContract::<i32>::produce_generic(&container, &ledger);

    println!("numeric={} textual={}", numeric, textual);
    println!("associated-type-body={}", times(&ledger, "associated-type-body"));
    println!("labeled-body={}", times(&ledger, "labeled-body"));
}
```

```
numeric=5 textual=title
associated-type-body=1
labeled-body=1
```

**TG8.** `Container` implements the same `Contract` with `Product = i32`, `Labeled` with
`Product = String`, and both sit side by side in the same source — this shows that the trait
itself is not bound to one type, only *that type's implementation* is. The constraint still
holds: neither `Container` nor `Labeled` can implement `Contract` a second time with a
different `Product`. The example also calls `GenericContract<i32>` on `Container` once more —
to show that two separate contracts (`Contract` and `GenericContract`) can coexist on the same
type in the same source, without excluding each other.

## Using It Inside Generic Code

The difference looks small while calling on a single concrete type (`Container`): both add
about three words, a difference of barely a few characters. The difference grows once `Contract`
or `GenericContract` is used **in the bound of another generic function**.

```rust
// generic-use.rs — using Contract in the bound of another generic function.
mod times;
use times::{times, record, Ledger, Container, Contract, GenericContract};
use std::cell::RefCell;
use std::collections::BTreeMap;
use std::rc::Rc;

// No need to write the Product type separately - T::Product is resolved by the compiler.
fn apply<T: Contract>(x: &T, ledger: &Ledger) -> T::Product {
    record(ledger, "generic-use-body");
    x.produce(ledger)
}

// The same idea with a generic parameter: C has to be ADDED to the signature SEPARATELY.
fn apply_generic<T: GenericContract<C>, C>(x: &T, ledger: &Ledger) -> C {
    record(ledger, "generic-use-generic-body");
    x.produce_generic(ledger)
}

fn main() {
    let ledger: Ledger = Rc::new(RefCell::new(BTreeMap::new()));
    let container = Container { value: 9 };

    let a = apply(&container, &ledger);
    let b = apply_generic(&container, &ledger);

    println!("apply={} apply_generic={}", a, b);
    println!("generic-use-body={}", times(&ledger, "generic-use-body"));
    println!("generic-use-generic-body={}", times(&ledger, "generic-use-generic-body"));
}
```

```
apply=9 apply_generic=9
generic-use-body=1
generic-use-generic-body=1
```

**TG7.** `apply`'s return type is `T::Product` — whatever `T` resolves to, its `Product` is
inherited automatically; there is no separate slot for `Product` in the signature, because none
is needed. `apply_generic`, after writing `T: GenericContract<C>` in its bound, is also forced
to add `C` to the function's **own** type parameter list — `C` cannot be inferred automatically
from `T`, because `T` could in principle carry more than one implementation of
`GenericContract`, and the compiler could not know which one is meant. This shows that the
difference at the call site from the previous section turns into a **signature** difference
when writing generic code: `apply`'s signature carries a single type parameter while
`apply_generic`'s carries two, even though the second function's only job is the same as the
first's.

The Trait Bounds lesson's `combine<T: Label, U: Label>` example showed how two independent type
parameters can stand side by side; `apply_generic<T: GenericContract<C>, C>` uses the same
notation, but here `C` is not an independent choice — it is a repetition of information `T`
already carries. The two functions fit syntactically into the same pattern — more than one
parameter inside `<...>` — but while in `combine` each parameter really answers a different
question, in `apply_generic`, `C` only asks the caller to rewrite information `T` already
carries. An associated type removes this repetition.

## The Measurement's Limit

An associated type adds a **constraint** to the contract: a type can implement `Contract` only
**once**, because `Product` is bound to a single value for that type. Once `Product = i32` is
declared for `Container`, a second `impl Contract for Container` block — a second one that
tries to make `Product` be `String` — is rejected by the compiler; this rejection is not put in a
code block, because source that does not compile is never shown as a code block in this
repository. The constraint itself is described in prose; the accepted counterpart is already
run above by `associated-type.rs` — there is a single `Contract` implementation for `Container`,
and the source compiles as it stands.

`GenericContract<C>` does not carry this same constraint: because `C` is part of the trait name,
`GenericContract<i32>` and `GenericContract<String>` are **two separate traits** to the
compiler, and `Container` can implement both at once.

```rust
// constraint.rs — a generic parameter allows more than one implementation for the same type;
// an associated type does not (one type = one Product binding).
trait GenericContract<C> {
    fn produce_generic(&self) -> C;
}

struct Container {
    value: i32,
}

impl GenericContract<i32> for Container {
    fn produce_generic(&self) -> i32 {
        self.value
    }
}

impl GenericContract<String> for Container {
    fn produce_generic(&self) -> String {
        self.value.to_string()
    }
}

fn main() {
    let container = Container { value: 9 };
    let numeric: i32 = GenericContract::<i32>::produce_generic(&container);
    let textual: String = GenericContract::<String>::produce_generic(&container);
    println!("numeric={} textual={}", numeric, textual);
}
```

```
numeric=9 textual=9
```

**TG6.** `Container` implements both `GenericContract<i32>` and `GenericContract<String>` at
the same time — both are accepted, both compile. This shows that the constraint an associated
type offers is not a shortcoming but a deliberate design choice: if `Contract` had been used
instead, `Container`'s `Product` would be fixed to a single value, and the caller would never
have to ask "which `Product`?" Using `GenericContract` buys this flexibility, but its cost shows
up at the call site: `container.produce_generic(&ledger)` alone is not enough, since `Container`
carries two implementations — which `C` is meant (`::<i32>` or `::<String>`) has to be written
explicitly. With an associated type, this statement is never needed, because there is only ever
one option.

This constraint should be read not as a shortcoming but as a **design tool**. If a contract
expects, by the nature of a type, to work with a single output type (say, "the element type it
holds" for a container, or "the result type it produces" for a converter), an associated type
turns this uniqueness into a syntax rule: the implementer cannot accidentally implement the same
type with two different output types, because the compiler does not allow it. A generic
parameter does not force this uniqueness — where a type genuinely can work with more than one
output form (as seen in this lesson, where a `Container` can have both a numeric and a textual
counterpart), it is the right tool. The reason both notations exist is that "one type = one
output" and "one type = more than one output" ask for different contracts.

## Summary

- An associated type is a type slot a trait carries within itself with a `type Name;`
  declaration, filled by the implementer; a generic parameter carries the same information
  inside the trait's name.
- Both notations fall into the every-time class and give exactly the same run count (three and
  three); the difference is not at run time, it is in the information the caller has to write in
  the signature.
- An associated type carries a constraint: a type can implement a trait with its associated
  type only once.
- A generic parameter does not carry this constraint: the same type can implement the same
  trait more than once with different parameter values — the cost is that the caller has to
  state explicitly which implementation is meant.

## Next Step

These two lessons showed that the run count does not change whether the same body is written
with a bound or with an associated type — both are resolved at compile time. The next lesson
turns to the question of **when** the call is resolved: is the same contract used with a call
resolved at compile time, or one resolved at run time, and does this pair change the run count?
