Generics and Type Parameters
Generics and Type Parameters
Generics let you write reusable code while preserving type information. They are essential for libraries, utilities, and data structures.
Item 71: Use Type Parameters to Capture Relationships
function pair<T>(first: T, second: T): [T, T] {
return [first, second];
}
const numbers = pair(1, 2); // [number, number]
const strings = pair("a", "b"); // [string, string]The key idea: inputs and outputs stay related.
Item 72: Prefer Fewer Type Parameters
Too many type params make APIs hard to understand:
// Overly complex
function zip<T, U, V>(a: T[], b: U[], c: V[]): Array<[T, U, V]> {
/* ... */
}
// Simpler alternative
function zip<T>(...arrays: T[][]): T[][] {
/* ... */
}When possible, reduce parameters and let inference do the work.
Item 73: Use Constraints to Express Requirements
function longest<T extends { length: number }>(a: T, b: T): T {
return a.length >= b.length ? a : b;
}Now any type with a length works: strings, arrays, or custom objects.
Item 74: Use Default Type Parameters
Defaults make APIs easier to call:
interface ApiResponse<T = unknown> {
data: T;
error?: string;
}
const res1: ApiResponse = { data: { raw: true } };
const res2: ApiResponse<User> = { data: { id: "1", email: "a@b.com" } };Item 75: Prefer Generics Over any
function identity<T>(value: T): T {
return value;
}This preserves the type instead of erasing it.
Item 76: Use extends to Narrow Type Parameters
function getId<T extends { id: string }>(item: T): string {
return item.id;
}Constraints also improve autocomplete and error messages.
Item 77: Prefer Generic Functions Over Generic Interfaces
Generic functions allow inference:
function map<T, U>(arr: T[], fn: (item: T) => U): U[] {
return arr.map(fn);
}If you force a generic interface, you often have to specify types explicitly.
Item 78: Use Generics to Model Data Structures
Typed Cache
class Cache<T> {
private store = new Map<string, T>();
get(key: string): T | undefined {
return this.store.get(key);
}
set(key: string, value: T): void {
this.store.set(key, value);
}
}
const userCache = new Cache<User>();
userCache.set('1', { id: '1', email: 'a@b.com' });
Item 79: Avoid Unnecessary Generic Abstractions
Generics are powerful, but don’t overuse them. If a function always works on User, don’t make it generic.
// Too generic
function getById<T extends { id: string }>(items: T[], id: string): T | undefined {
return items.find(i => i.id === id);
}
// Simpler and clearer if you only use Users
function getUserById(users: User[], id: string): User | undefined {
return users.find(u => u.id === id);
}Key Takeaways
- Use generics to express relationships between inputs and outputs.
- Keep type parameters minimal.
- Use constraints for required properties.
- Provide defaults when possible.
- Avoid over‑abstracting with generics.
Next: conditional and mapped types.