TypeScript63 min total · 21 parts
TypeScript Fundamentals: Types, Interfaces, Generics, and Why It Catches Bugs Before Runtime
Part 11 of 21 · ~2 min
Generics In Depth
function firstMatch<T>(items: T[], predicate: (item: T) => boolean): T | undefined {
return items.find(predicate);
}
firstMatch([8841, 8842, 8843], (id) => id > 8841); // T: number — result: number | undefined
firstMatch(["a", "b"], (s) => s === "b"); // T: string — result: string | undefined
The alternative to <T> here isn't really an alternative: it's either any[] in and any out, which throws away every guarantee the function could have offered, or a hand-copied version of firstMatch for every type you ever need it for. <T> lets one function serve every type while still keeping the promises specific to whichever type actually got used — call firstMatch([8841, 8842], (id) => id > 0)?.toFixed(2), and it compiles, because TypeScript inferred T as number for that particular call; try the same .toFixed(2) on the string-array call, and it's refused, because that call's T was string, and strings don't have .toFixed.
Functions aren't the only thing that can carry a type parameter — interfaces, type aliases, and classes all can too. The queue is a natural fit: it needs to hold Applicant objects, but "a line of things, first in first out" has nothing to do with applicants specifically.
class Queue<T> {
private items: T[] = [];
enqueue(item: T): void {
this.items.push(item);
}
dequeue(): T | undefined {
return this.items.shift();
}
get size(): number {
return this.items.length;
}
}
const reviewQueue = new Queue<Applicant>();
reviewQueue.enqueue({ id: 8841, name: "Priya Shah", email: "priya@example.com", stage: "screening" });
reviewQueue.enqueue("not an applicant");
// Error: Argument of type 'string' is not assignable to parameter of type 'Applicant'
Write Queue<T> once, and TypeScript enforces a different, correct rule for every particular instantiation of it — Queue<Applicant> won't take a bare string, Queue<string> wouldn't take an Applicant, and the class body itself never needs a single if to tell the two situations apart.
A type parameter list isn't capped at one, and later parameters are free to build on earlier ones:
function withScore<T, S>(applicant: T, scoreDetails: S): T & S {
return { ...applicant, ...scoreDetails };
}
const scored = withScore({ id: 8841, name: "Priya Shah" }, { score: 87, scoredAt: "2026-09-14" });
// scored: { id: number; name: string } & { score: number; scoredAt: string }