Why does a routine written against an uninspectable type parameter give its caller a stronger guarantee than the same routine written against a common base interface, and which languages let a generic body break that guarantee?
answer
- uninspectable T = can only rearrange, never invent
- free theorems read off the signature
- C# typeof(T) inside a generic is legal — leak
- Go: switch any(v).(type) / reflect — leak
- bound = budget of capabilities, not a door
basics
~20 sA body that cannot inspect its type parameter can only move values around — it cannot invent, compare or branch on them, and the caller gets the exact type back. Languages that expose type parameters at run time (C# typeof(T), Go via reflect or a type switch, Scala TypeTags) let the body cheat.
solid answer
~1 minTwo claims, one of which is a proof and one of which is only a hope. - **The parametric claim.** A signature like `forall T. List<T> -> List<T>` restricts the body to permuting, dropping and duplicating elements: it cannot construct a T, compare two Ts, or branch on what T is. Haskell enforces this closely, and Rust bodies see only what the declared bounds name — asking "is T a String?" requires an explicit `Any` bound and a `downcast_ref`. The caller also keeps the exact element type on the way out. - **The base-interface version has no such claim.** The body can downcast, branch on the concrete class, and hand back something the caller must cast. - **Where the guarantee leaks.** C# generics are reified, so `typeof(T) == typeof(string)` inside a generic method is legal and is a routine .NET specialization idiom. Go lets you write `switch any(v).(type)` or hand the value to `reflect`. Scala recovers the type with a `TypeTag`. Java's erasure blocks `T.class`, which is why Java APIs pass a `Class<T>` witness when they genuinely need the type. So: use a type parameter when you want uniformity proved; use an interface parameter when the routine must ask questions about the value.
code
csharp · 5 linesstatic string Describe<T>(T value) {
if (typeof(T) == typeof(string)) return "a string: " + value;
if (typeof(T) == typeof(int)) return "an int: " + value;
return "something else";
}go deeper
Know that a type parameter preserves the caller's exact type so no cast is needed on the way out.
Add the second, stronger claim: a body that cannot inspect T cannot behave differently per type, so whole classes of type-specific bugs are impossible.
Name where the guarantee leaks and how it looks in review — C# typeof, Go type switches, Scala TypeTags versus Rust and Haskell where the capability must be declared in the signature.
Use it as an API rule: uniform machinery takes a type parameter so uniformity is checkable, question-asking routines take an interface or a bound that names the capability, and escape hatches must be visible in the signature rather than buried in the body.
## The claim a type parameter makes When a routine is written as `forall T. f(List<T>) -> List<T>` and the body has no way to inspect T, the signature stops being documentation and becomes a restriction on what the code can possibly do. The body has no constructor for T, no equality on T, no ordering, no printing, no `instanceof`. Every T it returns must have arrived in the input. The consequence is that the whole family of implementations collapses into "some rearrangement of the input": reverse, take, drop, duplicate, identity. This property is called **parametricity**, and the theorems you can read off a signature for free are the "free theorems" of that signature. That is a materially stronger contract than the same routine written against a common base interface. `f(List<Shape>) -> List<Shape>` guarantees nothing about uniformity: the body may test whether an element is a circle, may fabricate a new shape, may return a list unrelated to the input. And at the call site the caller who passed circles gets shapes back and must cast. ## What the caller gets, concretely Two separate benefits are easy to conflate: 1. **Type preservation at the boundary.** The caller who passes a list of user ids gets a list of user ids, not a list of a supertype needing a downcast. This is the benefit everyone notices. 2. **Behavioural uniformity inside.** The routine cannot behave differently for one element type than another, so a bug that only shows up for one payload type is impossible by construction. This is the benefit that actually justifies preferring a type parameter over an interface parameter in a library. A bound weakens exactly claim 2 by exactly what it names. `T: Ord` grants comparison and nothing else, so a sort can be written but printing still cannot. That is a good property of bounds: they are a budget, not a door. ## Which languages keep the promise, and which sell you an escape hatch - **Haskell** comes closest to enforcing it. Adding capability means adding a constraint to the signature, and a `Typeable` constraint that would let you inspect the type is visible in that signature. - **Rust** gives the body only what the bounds declare. To branch on the concrete type you must bound by `Any` and call `downcast_ref`, which is visible at the definition and rarely idiomatic. - **C#** reifies generics: type arguments survive to run time, so `typeof(T) == typeof(string)` inside a generic method compiles and is used deliberately as a hand-rolled specialization in performance-sensitive .NET code. The guarantee is real only by convention there. - **Go** exposes the value to `reflect` or to `switch any(v).(type)`, so a generic body can special-case a type even though the constraint never mentioned it. - **Scala** can recover an erased type with an implicit `TypeTag`, again visible in the signature but easy to add. - **Java** erases type arguments, which blocks `T.class` but also blocks the legitimate uses, and that is precisely why Java APIs take a `Class<T>` witness argument when they need to construct or check a T. The witness argument is the honest version of the same power: it appears in the signature. The pattern across all six: the guarantee holds when the ability to inspect the type must be *declared*, and evaporates when the runtime hands it over for free. ## Why this is the real answer to "generics or a hierarchy?" The usual answer — "generics avoid casts" — is the small half. The design-level answer is about who is allowed to ask questions: - If the routine's correctness genuinely depends on what the element *is* — rendering, serialization, cost estimation, a policy per subtype — then the questions are the job, and an interface (or a trait bound naming exactly that capability) is the right parameter. Hiding that behind an uninspectable parameter just means smuggling the questions back in with reflection. - If the routine is a container, a pipeline stage, a cache, a retry wrapper or a scheduler, it should be uniform in its payload. A type parameter makes that uniformity checkable rather than merely intended, and the reviewer can see any escape hatch in the signature. A useful review heuristic follows from the language differences above: in C#, Go and Scala the constraint list is not the whole story, so a code reviewer must look inside the body for `typeof`, a type switch or reflection; in Rust and Haskell the signature is sufficient. That is the same abstraction in two languages carrying two different amounts of trust. ## Common misreadings Parametricity is not about performance and not about erasure versus reification as an implementation strategy; a reified implementation could in principle refuse to expose the type argument. It is about whether the body has a legal way to observe T. And it is not absolute anywhere: side channels such as identity-based operations, exceptions, and unsafe casts weaken it in every real language. Treat it as a strong default with visible exceptions, not a theorem you can bet safety on outside a pure language.
- Does adding a bound such as "T must be orderable" destroy the guarantee?It weakens it by exactly the operations the bound names and nothing more. With an ordering bound the body can compare and therefore sort, but it still cannot construct a T, print one, or branch on which concrete type arrived. That is why reviewers can read capability off a bound list in Rust or Haskell, and why a body that reaches for reflection despite a narrow bound is a design smell rather than a clever optimization.
- When is an interface parameter the better choice even in a language with good generics?When asking questions about the value is the routine's actual job — dispatching a rendering strategy, choosing a serializer, applying per-subtype policy. Forcing that behind an uninspectable type parameter only pushes the questions into reflection or a type switch, which is strictly worse because the capability disappears from the signature. Reach for a type parameter for payload-uniform machinery such as caches, queues and pipeline stages.
saying these in an interview costs you the question
- Reducing the whole benefit to "no casts" and missing that the caller also gains a uniformity guarantee about the body.
- Believing a generic body can never inspect its type argument — C# `typeof(T)`, Go type switches and Scala TypeTags all allow it.
- Treating erasure as the source of the guarantee; the guarantee comes from the body having no legal way to observe T, not from the runtime forgetting it.
- Adding reflection inside a generic body to special-case a type, then describing the routine as generic.
- Claiming a bound turns parametric code into subtype polymorphism; a bound grants named capabilities, it does not make the parameter a supertype.