In simple language, a generic is a placeholder for a type. We write the function once, and the caller (or the compiler) fills in the actual type. Think of it like a function parameter, but for types instead of values.
The classic example: an identity function that returns whatever it’s given.
// without generics — we lose type info
function identityBad(x: any): any {
return x;
}
const a = identityBad("hi"); // a: any — we lost "string"!
// with generics — type flows through
function identity<T>(x: T): T {
return x;
}
const b = identity("hi"); // b: string — TS inferred T = string
const c = identity(42); // c: number
The <T> is the type parameter. By convention we start with T, then U, V, etc. — but feel free to use descriptive names like <Key, Value>.
Why generics matter
They let us write reusable code WITHOUT falling back to any. The compiler still knows the real types, so autocomplete, refactoring, and error checking all keep working.
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const n = first([1, 2, 3]); // n: number | undefined
const s = first(["a", "b"]); // s: string | undefined
Multiple type parameters
We can have as many as we need. Common pattern: mapping one type to another.
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = pair("manish", 28); // p: [string, number]
Type inference vs explicit type arguments
Usually TS figures out the type from the arguments. If it can’t (or guesses wrong), we pass it explicitly.
function wrap<T>(x: T): T[] { return [x]; }
wrap(5); // T inferred as number
wrap<string>("hi"); // T explicit — useful when no arg helps inference
Generic function expressions
Same syntax, different position:
const map = <T, U>(arr: T[], fn: (x: T) => U): U[] =>
arr.map(fn);
const lengths = map(["abc", "de"], (s) => s.length);
// lengths: number[]
Generic in callback positions
This is where generics really shine — typing higher-order functions.
function withRetry<T>(fn: () => T, tries: number): T {
for (let i = 0; i < tries - 1; i++) {
try { return fn(); } catch {}
}
return fn(); // last try — let it throw
}
const result = withRetry(() => fetch("/api"), 3); // result: Response
Common interview question
“Write a typed version of Array.prototype.map.” That’s basically the map example above — and it’s a one-liner that tests both generics AND callback typing.
Gotcha: generics in arrow functions inside .tsx
In .tsx files, const f = <T>(x: T) => x is parsed as JSX. Workarounds: const f = <T,>(x: T) => x (trailing comma) or const f = <T extends unknown>(x: T) => x. Not relevant for .ts files.
Generics are the foundation for everything in the next two notes (constraints, generic classes), so this one’s worth getting solid.