RFA-489 · Case file with fixtures · Case 461 of 694 · Compiler evidence
Rust Associated-type Equality Belongs in a Bound, Not a Projection
Projection syntax names an associated item, while equality constraints restrict an implementation relationship. Write <I as Source>::Item in type position and put I: Source<Item = u8> in a generic bound or where clause.
- Reviewed
- Rust
- Rust 1.98.1, edition 2024
- Targets
- all Rust targets
- Profiles
- dev, release, test
Direct answer
What this Rust failure means
- Why it happens
- Projection syntax names an item while equality syntax restricts admissible implementations, and Rust assigns those roles to different grammar contexts.
- First discriminating check
- Project <I as Source>::Item and move I: Source<Item = u8> to the parameter bounds or where clause, then confirm I remains inferable.
I wrote <I as Source<Item = u8>>::Item in a parameter type. Rust emitted E0229 because an associated type equality constraint is not allowed inside that qualified projection path.
The failing fixture combines two pieces of syntax that each make sense elsewhere: projecting Item, and constraining Item = u8. Rust requires those jobs to appear in different grammatical positions.
A projection selects an item
<I as Source>::Item identifies the Item associated with I's implementation of Source. It names a type for use in a parameter, result, alias, or another type expression.
The qualified-path Reference explains this form. The trait portion is a path identifying the contract, not a location for arbitrary where-clause constraints.
Selection and restriction are related but not the same syntax.
Put equality on the implementation bound
The repaired fixture writes:
fn take<I>(value: &<I as Source>::Item)
where
I: Source<Item = u8>,
{}
The parameter projects the associated type. The where clause proves that the selected type equals u8.
The E0229 explanation also permits placing the constraint directly on a type parameter bound when that remains readable.
The bound may simplify the parameter type
Once I: Source<Item = u8> is known, the parameter could simply be &u8 if the projection relationship need not remain visible in the API.
Keeping <I as Source>::Item can communicate that the input belongs to the source contract and may help a generic signature evolve. Using u8 can make the immediate caller experience simpler.
I choose based on what relationship readers need to understand, not only what normalises to the same type.
Multiple equality constraints belong together
Traits often have Item, Error, and Context associated types. A where clause keeps their relationships in one place:
I: Source<Item = u8, Error = DecodeError>
Additional cross-trait equality can use separate bounds with fully qualified projections where necessary. I format complex constraints vertically and name intermediate type parameters when that improves diagnostics.
Dense angle-bracket syntax is not a measure of abstraction quality.
Equality is a compile-time proof
The bound does not convert an arbitrary I::Item into u8 at runtime. It limits valid instantiations of I to implementations whose associated choice is already u8.
If conversion is intended, I use Into<u8>, TryInto<u8>, or a domain mapping bound rather than equality. Those operations have different failure and ownership behaviour.
This distinction matters for generic API flexibility.
Trait objects use binding syntax in their own allowed position
dyn Source<Item = u8> uses associated type binding as part of the trait object bound. That does not make the same binding valid inside <I as ...>::Item.
Rust grammar assigns constraints to trait-bound contexts. I identify whether I am defining an admissible implementor, creating a trait object, or projecting an item before placing Item = Type.
The trait-bound Reference is the authoritative syntax guide.
Type aliases can contain repeated complexity
If a projection appears throughout a module, a local alias can improve errors and readability:
type SourceItem<I> = <I as Source>::Item;
The alias itself may require appropriate bounds at use sites. It should name a recurring domain concept, not hide an important equality behind a vague Value name.
My E0229 checklist
I also check inference at the call site after moving the bound. If I appears only through a normalised u8 parameter, callers may have no information from which to infer which Source type is intended. The function might need an explicit source value, a marker parameter, or a clearer API shape. A valid where clause can still leave a type parameter unconstrained. I make sure every generic parameter represents a choice the caller can actually supply or the compiler can infer.
- Am I projecting an associated item or constraining an implementation?
- Can I remove the binding from
<I as Trait>? - Where should
I: Trait<Item = Type>be stated? - Would the concrete normalised type be clearer in the parameter?
- Are several equalities easier to read in a where clause?
- Do I need equality or a conversion capability?
- Is this actually a trait-object bound context?
- Would a well-named alias reduce repeated projections?
The core principle is that type projection answers “which associated item?” while a bound answers “which implementations are permitted?” I keep those statements in their proper positions so generic contracts remain readable and checkable.