The idea
What it is
Generics (called templates in C++) let you write code once and reuse it for many types — a Stack, a List, a Box — without rewriting it per type and without throwing away type safety. The type becomes a parameter you fill in later, like an argument, but for types instead of values.
Think of a labelled shipping box. The box design is identical no matter what goes inside, but once you write "BOOKS" on the side, everyone knows only books belong in it — and a mismatch gets caught at the loading dock, not after the truck has left. A generic Box<T> is exactly that: one design, but each box you create is labelled with the type it holds.
The one sentence to remember
Generics move type errors from runtime (a crash when a user clicks the button) to compile time (a red squiggle before you even run the code) — all while writing the container once.
Mechanics
How it works
The problem: untyped containers force casts and crash late
Before generics, a reusable container held anything — Object in Java, any in TypeScript, void* in C. That sounds flexible, but it has two ugly costs. First, everything you take out has lost its type, so you must cast it back and hope you guessed right. Second, nothing stops you putting the wrong thing in — the mistake only surfaces later, as a runtime crash, often far from where the bug was written:
// Pre-generics: a box of Object holds anything — and lies to you.
List box = new ArrayList(); // raw, untyped
box.add("hello"); // a String slips in by mistake
box.add(42); // ...so does an int
Integer n = (Integer) box.get(0); // cast — compiles fine
// ClassCastException at RUNTIME: "hello" is not an Integer.The other escape hatch is just as bad: writing the same class twice — an IntStack, then a StringStack, then a UserStack — each a copy-paste with one type swapped. Now a bug fixed in one is still alive in the other two.
The fix: a type parameter `T`
A generic introduces a type parameter — a placeholder, conventionally named T — that you fill in when you use the type. You write Box<T> once; callers create Box<number>, Box<string>, or Box<User>. Inside the class, T stands for whichever type the caller chose, so push accepts a T and pop returns a T — no casting on the way out.
class Box<T> { // T is a type parameter
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
peek(): T | undefined { return this.items[this.items.length - 1]; }
}
const nums = new Box<number>();
nums.push(42); // ok
const n = nums.pop(); // n is number — no cast neededCompile-time safety: the wrong type is rejected before it runs
This is the payoff. Once a box is Box<number>, the compiler knows push only takes a number. Hand it a string and the program never even runs — you get an error at your desk, not a 2am page:
const nums = new Box<number>();
nums.push("oops");
// ✗ Compile error: Argument of type 'string'
// is not assignable to parameter of type 'number'.Bounded type parameters: "any T, but it must be able to…"
Sometimes "any type" is too loose — you need a T that supports a particular operation. A bounded type parameter constrains it. T extends Comparable means "any type, as long as it can be compared" — so inside the generic you can safely call a.compareTo(b). You keep the reuse, but the compiler guarantees the capability you depend on.
// T must be Comparable, so a.compareTo(b) is guaranteed to exist.
static <T extends Comparable<T>> T max(T a, T b) {
return a.compareTo(b) >= 0 ? a : b;
}
max(3, 7); // ok — Integer is Comparable
max("ant", "bee"); // ok — String is Comparable
// max(new Thread(), new Thread()); // ✗ Thread isn't ComparableErasure vs. real instantiation — a key difference across languages
How generics work under the hood differs. Java uses type erasure: T is checked at compile time, then erased — Box<number> and Box<string> are the same class at runtime, so you can't do new T() or instanceof T. TypeScript also erases types entirely — none of it exists at runtime. C++ templates are the opposite: the compiler instantiates a fresh, fully-typed copy for each type you use, so Box<int> and Box<string> are genuinely different generated classes. That power is why C++ templates can be faster but also produce code bloat and famously cryptic errors.
Interactive prototype
See it. Build it. Break it.
A sandboxed, hands-on simulation — no setup, no install. Play with it as you read.
About this simulation
Pick a type for the box — Box<number>, Box<string>, or Box<User> — then try to put a value in. A matching value slides in (green); a wrong type is rejected before it runs with a simulated compile error. Flip on the raw Box<any> to see the opposite trade: it swallows anything, then blows up at runtime when you use it.
Hands-on
Try these yourself
Open the prototype above, predict what happens, then verify.
Type the box, then put a match in
Leave the dropdown on Box<number> (the seed) and type 7 into the input, then press box.push(). It lands in the box and the console logs box.push(42): ok in green. The box accepts exactly the type it was created with.
Try the wrong type — caught at compile time
With Box<number> still selected, type a word like hello and press box.push(). A red banner appears: ✗ Compile error: Argument of type 'string' is not assignable to 'number', and nothing is added to the box. The mistake is stopped before the program runs.
Flip to the raw Box<any> and watch it blow up late
Toggle Use raw Box<any> on. Now any value is accepted with no complaint. Push a string, then press use item — .toFixed(). With no compile-time check, the bad value sails through and throws a runtime error instead. Same mistake, much later — that's the cost generics remove.
In practice
When to use it — and what trips people up
Reach for generics when…
- You're writing a container or collection — a stack, queue, cache, list, tree — that should work for any element type without you rewriting it per type.
- You catch yourself copy-pasting a class and swapping one type (
IntStack→StringStack). That duplication is the signal: parameterize it withTinstead. - An algorithm is shape-agnostic —
max,swap,map,filter— and only needs the element to support a small capability. Use a bounded parameter (T extends Comparable) to require exactly that capability and no more.
Don't reach for <T> by reflex
If a type only ever holds one concrete type, a plain non-generic class is clearer. Generics earn their keep when the same logic genuinely serves many types. A signature crammed with <T, U, V extends Foo<T>> for a one-off helper costs more readability than the reuse buys back.
What it gives you
- Write the container or algorithm once and reuse it for every type — no copy-pasted IntStack/StringStack variants.
- Compile-time type safety: a wrong-type insert is rejected before the program runs, not as a runtime crash.
- No casting on the way out —
pop()already returnsT, so you keep the real type with no(Integer)casts. - Self-documenting APIs:
Box<User>states exactly what it holds, so callers and tools can't get it wrong.
Common mistakes
- Over-generic signatures hurt readability — too many type parameters and bounds turn a simple helper into a puzzle.
- Java erasure limits:
Tis gone at runtime, so you can't donew T(),instanceof T, or make aT[]directly. - C++ template bloat & cryptic errors: each instantiation generates real code (binary size), and a tiny mismatch can spew pages of unreadable compiler output.
- Easy to over-reach: forcing
<T>onto code that only ever uses one type adds ceremony with no payoff.
Reference
Code & further reading
A minimal reference implementation and pointers worth bookmarking.
// One generic class, reused for many types. Pick your language above.
class Box<T> {
private items: T[] = [];
push(item: T): void { this.items.push(item); }
pop(): T | undefined { return this.items.pop(); }
peek(): T | undefined { return this.items[this.items.length - 1]; }
}
const nums = new Box<number>();
nums.push(42); // ok
const n = nums.pop(); // n: number | undefined — no cast
const words = new Box<string>();
words.push("hi"); // ok
nums.push("oops");
// ✗ Compile error: Argument of type 'string' is not
// assignable to parameter of type 'number'.References & further reading
5 sources- Docsdocs.oracle.com
Oracle — Generics (Java Tutorial)
The canonical introduction to Java generics: type parameters, bounded types, wildcards, and erasure.
- Docstypescriptlang.org
TypeScript Handbook — Generics
Generic functions, classes, constraints (
extends), and default type parameters, with runnable examples. - Docsen.cppreference.com
cppreference — Templates
Reference for C++ class and function templates — the compile-time instantiation model behind
template<class T>. - Docsdocs.python.org
Python docs — typing.Generic
How
Generic[T]andTypeVardeclare generic classes that static checkers like mypy and pyright enforce. - Articleen.wikipedia.org
Wikipedia — Generic programming
A broad, language-agnostic map of generics, templates, and parametric polymorphism across languages.
Knowledge check
Did it land?
Quick questions, answers revealed on submit. Nothing is scored or saved.
question 01 / 05
What is the main benefit of a generic Box<T> over an untyped Box that holds Object/any?
question 02 / 05
You have Box<number> b. What happens when you write b.push("hello") in a typed language?
question 03 / 05
What does a bounded type parameter like <T extends Comparable<T>> give you?
question 04 / 05
Which statement about how generics work at runtime is correct?
question 05 / 05
An untyped Box<any> accepts a string, and later code calls .toFixed() on the popped value expecting a number. When does this fail?
0/5 answered