Mehdi Akiki
Rust Failure Atlas / Language and diagnostics

RFA-375 · Case file with fixtures · Case 347 of 694 · Compiler evidence

A Rust Type Alias Cannot Carry an Unused Generic Identity

A type alias is another name for its right-hand type, not a nominal wrapper. Remove a decorative parameter, or use a newtype carrying PhantomData when UserId and JobId must remain different types.

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
A type alias creates another name for its right-hand type and cannot preserve a compile-time distinction through a parameter that does not occur there.
First discriminating check
Expand the alias mentally and either remove the unused parameter or introduce a real wrapper carrying PhantomData.

I once tried to make IDs safer with an alias like type Id<T> = u64. My intention was that Id<User> and Id<Job> would become different types. Rust reported E0091 because T was unused. The compiler was also showing that the proposed alias could not provide the identity I wanted.

The failing fixture defines CacheKey<T> as u64. Nothing on the right-hand side mentions T, so rustc rejects the unnecessary parameter.

An alias does not create a new nominal type

The type-alias Reference describes an alias as a new name for an existing type. After expansion, CacheKey<String> would simply be u64. It would have the same methods, layout, trait implementations, and compatibility as every other u64 alias.

The generic spelling on the left cannot preserve a distinction that disappears entirely on the right. Allowing an unused T would make code look typed by domain while still permitting accidental mixing after normalization.

This differs from a generic struct. A struct declaration creates a nominal type, and its generic arguments participate in that type's identity even if they are represented only through a marker field.

Remove the parameter when it is only decoration

If all keys are genuinely interchangeable numbers, the honest repair is type CacheKey = u64. The alias can improve readability, but I do not claim it enforces separation.

I use aliases for long composite types, callback signatures, or vocabulary where substitutability is desired. They help humans and reduce repetition. They are not validation boundaries.

The E0091 explanation recommends removing unnecessary parameters or referring to them in the alias body. Adding T through an irrelevant tuple only to silence the diagnostic would change the represented type and usually create awkward runtime data.

Use a newtype for domain separation

The repaired fixture defines a CacheKey<T> struct with a u64 and PhantomData<fn() -> T>. Now CacheKey<String> and CacheKey<Vec<u8>> are distinct nominal types while the marker occupies no runtime storage.

The private fields force construction through new, giving the type a place to validate ranges or reserve values later. Traits can be implemented intentionally rather than inherited automatically from u64.

I often use this pattern for database IDs, units, handles, and state-indexed tokens. Passing a UserId where an InvoiceId is expected becomes a compile error even though both store the same integer representation.

PhantomData has semantic effects

PhantomData tells the compiler that the wrapper acts as if it uses a type or lifetime. This can affect variance, drop checking, and automatically derived Send or Sync behavior. It is not only a way to silence E0091.

The marker shape matters. PhantomData<T>, PhantomData<*const T>, and PhantomData<fn() -> T> express different relationships. For a typed numeric tag that does not own T, I choose a marker deliberately and document it if concurrency or variance can matter.

If I do not understand those implications, a simpler concrete marker enum per domain may be clearer than a highly generic wrapper.

Layout must be requested separately

A one-field newtype often has the layout I expect, but FFI and transparent layout promises should be explicit. When ABI equivalence with the field is required and the rules fit, #[repr(transparent)] can provide that contract.

For ordinary application IDs I avoid adding representation attributes without need. Serialization libraries also need an explicit choice: encode the inner number for compatibility, or include type information at a higher schema layer. Rust's nominal distinction does not travel automatically into JSON, SQL, or a message broker.

Methods can preserve invariants aliases cannot

Once a newtype exists, I can expose get, checked parsing, display formatting, and conversion at chosen boundaries. I can avoid implementing arithmetic if adding two IDs has no domain meaning. A plain alias inherits every integer operation and makes invalid expressions easy to write.

This is one reason I do not view the wrapper as boilerplate. It is a small local type system for a domain promise.

At system boundaries, I parse raw values into the wrapper and return an error for invalid values. Inside the system, functions accept the specific wrapper. That converts repetitive runtime checks into a construction invariant.

My review question is whether substitution is allowed

When I see an alias, I ask whether any value of the right-hand type should be accepted everywhere the alias appears. If yes, an alias is suitable. If no, a nominal wrapper is needed.

For generic aliases I expand the right-hand side and circle every parameter occurrence. A missing parameter may be a typo, an over-generalized API, or evidence that the author expected nominal behavior from an alias.

The core principle is that a name alone does not create identity. E0091 prevents a generic parameter from pretending to distinguish aliases when the expanded type erases it. I remove decorative parameters and introduce a real wrapper when the distinction protects the program.