Generics — the fundamentals
A generic isn't a vague "any type" — it's a placeholder that lets a function or type stay precise about the relationship between its inputs and outputs, without committing to one specific type ahead of time.
4 min read
The problem generics solve, shown by what goes wrong without them
function firstElementAny(arr: any[]): any {
return arr[0];
}
const nums = [1, 2, 3];
const first = firstElementAny(nums); // typed as `any` — every bit of type safety is gone
first.toUpperCase(); // no error! — `any` doesn't catch that numbers have no toUpperCaseTyping the parameter and return value as any "works" in the sense that it compiles for any input — but it throws away everything TypeScript could have told you about the relationship between what went in and what came out. first should obviously be a number, since nums is number[], but any erases that connection entirely, letting the nonsensical first.toUpperCase() compile without complaint.
Generics: a type parameter that preserves that exact relationship
function firstElement<T>(arr: T[]): T {
return arr[0];
}
const nums = [1, 2, 3];
const first = firstElement(nums); // TypeScript infers T = number automatically
first.toUpperCase(); // Error: Property 'toUpperCase' does not exist on type 'number'
const words = ["a", "b"];
const firstWord = firstElement(words); // T = string, inferred separately, per call<T> declares a type parameter — a placeholder standing in for "whatever type this specific call actually uses." Unlike any, T isn't a blank check that disables checking; it's a variable that gets bound to a real, specific type on every call, and TypeScript then holds every other use of T in that same function accountable to whatever it was bound to — arr: T[] and the return type T are linked, so the return type is always genuinely the element type of whatever array was actually passed in, not a vague "could be anything."
T is inferred automatically most of the time — you rarely write it explicitly
const first = firstElement(nums); // T inferred as number, from the argument
const firstExplicit = firstElement<number>(nums); // same result, T written out explicitly
const empty: number[] = [];
const firstEmpty = firstElement<number>(empty); // here, explicit T genuinely helps —
// nothing about an empty array alone
// would let TypeScript infer T on its ownExplicit type-parameter syntax (firstElement<number>(...)) exists and is sometimes necessary, but in the overwhelming majority of real calls, TypeScript infers T correctly from the arguments actually passed, the same inference mechanism the basic-types lesson covers for ordinary variables — writing it out explicitly is really only needed when there's genuinely nothing in the arguments for TypeScript to infer T from at all.
Generic types, not just generic functions
interface Box<T> {
contents: T;
}
const numberBox: Box<number> = { contents: 42 };
const stringBox: Box<string> = { contents: "hello" };
function unwrap<T>(box: Box<T>): T {
return box.contents;
}interfaces and type aliases can be generic too, not just functions — Box<T> is a reusable "container" shape, parameterized over whatever it actually holds. Box<number> and Box<string> are two different, fully specific types generated from the same generic definition — this is the exact mechanism behind TypeScript's own built-in Array<T> (number[] is really just shorthand for Array<number>), Promise<T>, and Map<K, V>.
Multiple type parameters
function pair<A, B>(first: A, second: B): [A, B] {
return [first, second];
}
const p = pair("Ada", 36); // inferred as [string, number]A generic can declare more than one type parameter, each independently inferred from wherever it's actually used — A from the first argument, B from the second, combined into a genuinely typed tuple [A, B]. This is how a function or type can stay precise about multiple independent relationships between its inputs and its output at once, not just one.
What generics are NOT: they don't make a function work on more types than it actually handles
function double<T>(x: T): T {
return x * 2; // Error: The '*' operator cannot be applied to types 'T' and 'number'
}A common early misconception: writing <T> doesn't mean "this function now works generically on numbers, strings, whatever" — it means "this function works on some type T, and I have to write code that's actually valid for any possible T," which x * 2 genuinely isn't, since T could be a string or an object where * makes no sense at all. Generics preserve type relationships; they don't grant permission to use operations that only some possible types support — that's exactly what the next lesson's generic constraints are for.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Given `function firstElement<T>(arr: T[]): T { return arr[0]; }`, what does calling it with a `number[]` actually give you, type-wise?
2. Why does `function double<T>(x: T): T { return x * 2; }` fail to compile?
3. What does `Box<number>` and `Box<string>` (from a generic `interface Box<T> { contents: T }`) represent?