Generics & Utility Types

Difficulty: Intermediate

Question

Explain generics in TypeScript. Implement a type-safe function that works with any data type.

Answer

Generics let you write reusable code that works with any type while maintaining type safety. Instead of using 'any' (which disables type checking), generics preserve the actual type.

Key concepts: - Type parameters: <T>, <K, V> - Constraints: <T extends object> - Default types: <T = string> - Built-in utility types: Partial, Required, Pick, Omit, Record, Readonly

Code examples

Generic Functions

// Without generics - loses type info
function firstBad(arr: any[]): any {
  return arr[0];
}
const x = firstBad([1, 2, 3]); // type: any

// With generics - preserves type
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
const a = first([1, 2, 3]);       // type: number
const b = first(['a', 'b']);       // type: string
const c = first<boolean>([true]);  // type: boolean

// Constrained generic
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}
const user = { name: 'Alice', age: 30 };
getProperty(user, 'name');  // OK, returns string
// getProperty(user, 'foo'); // Error: 'foo' not in keyof user

Generics capture the actual type and carry it through. Constraints (extends) limit what types are acceptable.

Built-in Utility Types

interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}

// Partial<T> - all properties optional
type UpdateUser = Partial<User>;
// { id?: number; name?: string; email?: string; age?: number; }

// Pick<T, K> - select specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: number; name: string; }

// Omit<T, K> - exclude properties
type CreateUser = Omit<User, 'id'>;
// { name: string; email: string; age: number; }

// Record<K, V> - create object type from keys & values
type UserRoles = Record<string, 'admin' | 'user' | 'guest'>;
// { [key: string]: 'admin' | 'user' | 'guest' }

// Readonly<T> - all properties readonly
type FrozenUser = Readonly<User>;
// All props are readonly

Utility types transform existing types. They're essential for reducing duplication and keeping types in sync.

Implementing a Generic API Response

// Generic API response wrapper
type ApiResponse<T> = {
  data: T;
  status: number;
  message: string;
  timestamp: Date;
};

type PaginatedResponse<T> = ApiResponse<T[]> & {
  pagination: {
    page: number;
    totalPages: number;
    totalItems: number;
  };
};

// Usage
async function fetchUsers(): Promise<PaginatedResponse<User>> {
  const res = await fetch('/api/users');
  return res.json();
}

// The consumer gets full type safety
const response = await fetchUsers();
response.data[0].name;          // OK: string
response.pagination.totalPages;  // OK: number

Generic response types ensure type safety across your entire API layer. Define once, reuse everywhere.

Key points

Concepts covered

Generics, Constraints, Partial, Pick, Omit, Record, Conditional Types