Mastering TypeScript Generics — From Basics to Advanced Patterns
A comprehensive guide to TypeScript generics covering type parameters, constraints, conditional types, mapped types, and real-world utility patterns.
TypeScriptProgrammingWeb Dev
Why Generics?
Generics are the backbone of reusable, type-safe code in TypeScript. They let you write functions and types that work with any data type while preserving full type information.
Without generics, you're stuck choosing between type safety and reusability:
// ❌ Type-safe but not reusable
function firstNumber(arr: number[]): number | undefined {
return arr[0];
}
// ❌ Reusable but not type-safe
function firstAny(arr: any[]): any {
return arr[0];
}
// ✅ Both type-safe AND reusable
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // type: number | undefined
const str = first(["a", "b", "c"]); // type: string | undefinedConstraints with extends
You can restrict what types a generic accepts using the extends keyword:
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): T {
console.log(`Length: ${item.length}`);
return item;
}
logLength("hello"); // ✅ strings have length
logLength([1, 2, 3]); // ✅ arrays have length
logLength(42); // ❌ numbers don't have lengthConditional Types
Conditional types let you create types that depend on other types:
type ApiResponse<T> = T extends string
? { message: T }
: T extends object
? { data: T }
: { value: T };
type A = ApiResponse<string>; // { message: string }
type B = ApiResponse<User>; // { data: User }
type C = ApiResponse<number>; // { value: number }Real-World Utility: Type-Safe Event Emitter
Here's a practical pattern combining multiple generic features:
type EventMap = {
userLogin: { userId: string; timestamp: Date };
pageView: { path: string; referrer?: string };
error: { code: number; message: string };
};
class TypedEmitter<T extends Record<string, unknown>> {
private handlers = new Map<keyof T, Set<(payload: any) => void>>();
on<K extends keyof T>(event: K, handler: (payload: T[K]) => void) {
if (!this.handlers.has(event)) {
this.handlers.set(event, new Set());
}
this.handlers.get(event)!.add(handler);
}
emit<K extends keyof T>(event: K, payload: T[K]) {
this.handlers.get(event)?.forEach((fn) => fn(payload));
}
}
const emitter = new TypedEmitter<EventMap>();
emitter.on("userLogin", ({ userId }) => {
// userId is typed as string ✅
});
emitter.emit("error", { code: 404, message: "Not found" }); // ✅Summary
| Concept | Use Case |
|---|---|
Basic <T> | Reusable functions/components |
extends constraints | Restricting accepted types |
| Conditional types | Type-level branching |
| Mapped types | Transforming object shapes |
infer | Extracting types from patterns |
Generics are a superpower. Once you internalize these patterns, you'll write TypeScript that's both safer and more expressive.