Learning Guides
Menu

Generics and Type Parameters

3 min readEffective TypeScript

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

TYPESCRIPT
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:

TYPESCRIPT
// 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

TYPESCRIPT
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:

TYPESCRIPT
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

TYPESCRIPT
function identity<T>(value: T): T {
  return value;
}

This preserves the type instead of erasing it.

Item 76: Use extends to Narrow Type Parameters

TYPESCRIPT
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:

TYPESCRIPT
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

TYPESCRIPT
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.

TYPESCRIPT
// 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

  1. Use generics to express relationships between inputs and outputs.
  2. Keep type parameters minimal.
  3. Use constraints for required properties.
  4. Provide defaults when possible.
  5. Avoid over‑abstracting with generics.

Next: conditional and mapped types.