Generics
In JavaScript, a function that takes "anything" and returns "the same thing" just... works,
because nothing is checked either way. The moment you add types, that flexibility seems to
disappear — do you write one identity function per type? Generics
are TypeScript's answer: a way to write one function or class that works with many types,
while still keeping the types connected to each other.
💡 The Bottom Line
A generic is a type parameter — conventionally named T — written in angle
brackets right after a function or class name: function identity<T>(value: T): T.
It means "whatever type gets passed in for T at the call site, use that exact
same type for every other place T appears." You get the reusability of untyped
JavaScript back, without losing type safety.
An identity function, with and without generics
function identity(value) {
return value;
}
identity(5); // 5
identity("hi"); // "hi"
// works for anything, but nothing
// is checked or preserved
function identity(value: any): any {
return value;
}
let x = identity(5);
x.toUpperCase();
// compiles! any disables all
// checking on the result
function identity<T>(value: T): T {
return value;
}
let x = identity(5);
// x is inferred as number
x.toUpperCase();
// compile error: number has no
// method toUpperCase
let y = identity("hi");
// y is inferred as string
y.toUpperCase();
// fine — y really is a string
📋 Key differences
<T>after the function name declares a type parameter, scoped to that function.Tis just a placeholder name — it could be<Item>or<Value>, butT,U,K,Vare the common single-letter conventions.- You almost never have to write
identity<number>(5)explicitly — TypeScript infersTfrom the argument you pass. - Using
anyinstead of a generic also "works" for every type, but it throws away the connection between input and output type entirely — that's the whole difference.
A typed wrapper: generics on a class
Generics show their real value on data structures that hold a value of some type and hand it back later — a box, a stack, a cache entry. In plain JavaScript, such a wrapper accepts and returns anything, so a bug where you push a number into a box of strings won't surface until something downstream calls a string method on it.
class Box {
constructor(value) {
this.value = value;
}
get() {
return this.value;
}
}
const box = new Box("hello");
box.value = 42; // no error, ever
box.get().toUpperCase(); // crashes
// at runtime
class Box<T> {
constructor(private value: T) {}
get(): T {
return this.value;
}
}
const box = new Box<string>("hello");
box.get().toUpperCase(); // fine
const numBox = new Box(42);
// T inferred as number
numBox.get().toUpperCase();
// compile error: number has no
// method toUpperCase
Notice new Box("hello") alone would also infer T as
string — the explicit <string> is only needed when TypeScript
has nothing to infer from, such as an empty array: new Box<string[]>([]).
⚠️ Gotcha
An unconstrained T gives TypeScript no information about what operations are
valid on it — inside a generic function, you can't call value.length or
value.toUpperCase() on a plain T, because TypeScript has to allow
for T being a number, a boolean, anything. If your generic function needs to rely
on a property existing, constrain it: function printLength<T extends { length:
number }>(value: T). Forgetting the constraint and reaching for a property anyway is
the single most common "why won't this compile" moment when learning generics.
Built-in generics you already use, whether you know it or not
Arrays are themselves a generic type — string[] is shorthand for
Array<string>. Once you notice this, generics stop looking like an exotic
feature and start looking like the mechanism behind most of the type safety you get for free
from the standard library, including Promise<T>, Map<K, V>,
and Set<T>.
let names: string[] = ["Ada", "Grace"];
let scores: number[] = [10, 20];
let names: Array<string> = ["Ada", "Grace"];
let scores: Array<number> = [10, 20];
🔧 Try It Yourself
Write a generic function firstOf<T>(items: T[]): T that returns the
first element of any array. Call it with a string[] and a number[]
and confirm TypeScript infers the correct return type each time. Then try calling
.toFixed(2) (a number-only method) on the result when you passed a
string[] — confirm the compiler stops you.
🙋 There Are No Dumb Questions
Q: Isn't a generic basically the same thing as any, since both accept
any type?
A: They accept any type going in, but they behave completely differently going out.
any disables checking on the value forever, at every point it flows through.
A generic T keeps track of exactly which type was passed for that specific call,
and keeps checking against that type everywhere else T is used in the same call —
that link between input and output is exactly what any throws away.
✅ Quick Check
Given function wrap<T>(value: T): T[] { return [value]; }, what is the
inferred type of wrap(42)?
Next up: real-world values are often "this OR that" — a response that's either a result or an error, an id that's a string or a number. Union types and narrowing are how TypeScript handles that safely.