Concept

Generics and Parametric Polymorphism

Generics let one definition work over many types while preserving type information, but languages differ in constraints, variance, runtime representation, and code generation strategy.

What Generics Are For

Generics let a function, type, interface, trait, protocol, class, or module abstract over types. Instead of writing separate versions of a list, map, parser, HTTP response, optional value, or result type for every concrete value, the language lets one definition describe the shared structure.

Parametric polymorphism is the core idea: code is written over a type parameter and should work uniformly for any type that satisfies the required constraints. In practice, languages add bounds, interfaces, traits, type classes, variance rules, runtime metadata, and escape hatches.

Constraints

Generic code usually needs to say what it assumes about a type parameter:

  • Java and C# use generic constraints and interfaces.
  • Rust uses trait bounds.
  • Go uses type parameter constraints and interfaces.
  • TypeScript uses structural constraints with extends.
  • Haskell uses type classes.
  • OCaml and F# combine inference with explicit module, object, interface, or member constraints where needed.

The constraint is the contract. A function that only stores and returns values needs little. A function that compares, formats, serializes, clones, awaits, orders, or adds values needs a capability the checker can see.

Representation Choices

Languages implement generics differently.

Java generics are mostly erased, preserving compatibility with older bytecode while limiting runtime access to parameterized type information. .NET generics are represented in runtime metadata and are central to reflection and framework APIs. Rust often monomorphizes generic code into concrete instantiations, trading larger generated code in some cases for direct static dispatch. TypeScript generics disappear with the rest of the type system when JavaScript is emitted.

These choices affect reflection, binary compatibility, performance, error messages, library APIs, and how much generic information survives at runtime.

Variance And Higher Abstractions

Variance describes when a generic type can be substituted for another generic type. Collections, callbacks, mutable containers, and inheritance make this subtle. Java wildcards, C# variance annotations, Kotlin declaration-site variance, and TypeScript structural assignability all answer this problem differently.

Some languages also support higher-kinded or type-constructor abstractions directly or through extensions and patterns. Haskell and Scala make this more central. F#, OCaml, Rust, C#, and TypeScript can express some adjacent ideas through modules, traits, interfaces, associated types, or library conventions, but the ergonomics vary widely.

Watch Points

Generics can remove duplication and make APIs safer. They can also turn simple code into a maze of type parameters, bounds, helper traits, phantom types, and unreadable errors.

Use generics when the caller meaningfully benefits from one reusable abstraction. Prefer concrete types when the domain is concrete. Add type parameters only when the variability is real and the constraint can be explained in ordinary language.

Sources

Last verified:

  1. Generic Data Types - The Rust Programming Language Rust Project
  2. Generic Types and Methods - C# Microsoft Learn
  3. Type Erasure - The Java Tutorials Oracle
  4. Generics - TypeScript Handbook Microsoft
  5. Haskell 2010 Language Report - Declarations and Bindings Haskell.org