05 - Generics

What are Generics?

Generics let you write reusable code that works with any type, while still keeping type safety. Instead of using any, you use a type variable (like T) that gets filled in when you use the function/class.

Basic Generic Function

// Without generics — loses type info
function identity(arg: any): any {
  return arg;  // returns any — TypeScript forgets the type
}

// With generics — preserves type info
function identity<T>(arg: T): T {
  return arg;  // returns the same type that was passed in
}

const num = identity<number>(42);   // T = number, returns number
const str = identity("hello");      // T inferred as string, returns string

<T> is a placeholder — it gets replaced with the actual type when you call the function.

Generic Interfaces

interface Box<T> {
  value: T;
}

const numberBox: Box<number> = { value: 123 };
const stringBox: Box<string> = { value: "hello" };

Generic Classes

class DataStore<T> {
  private data: T[] = [];

  add(item: T): void {
    this.data.push(item);
  }

  get(index: number): T | undefined {
    return this.data[index];
  }

  getAll(): T[] {
    return [...this.data];
  }
}

const users = new DataStore<string>();
users.add("Alice");
users.add(42);  // ❌ error — only strings allowed

Generic Constraints

Limit what types T can be using extends:

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(arg: T): void {
  console.log(arg.length);
}

logLength("hello");     // ✅ string has .length
logLength([1, 2, 3]);   // ✅ array has .length
logLength(123);          // ❌ error — number doesn't have .length

The keyof Operator

keyof takes an object type and produces a union of its keys:

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

type UserKeys = keyof User;  // "id" | "name" | "email"

It's especially useful with generics to create type-safe property access:

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key];
}

const user = { id: 1, name: "Alice", email: "a@b.com" };

getProperty(user, "name");    // ✅ returns string
getProperty(user, "age");     // ❌ error — "age" is not a key of User

K extends keyof T means K can only be one of T's actual keys — TypeScript catches typos and invalid keys at compile time.

Multiple Type Parameters

function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

const result = pair("age", 30);  // [string, number]

Real-World Example: API Response

interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
}

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

interface Post {
  id: number;
  title: string;
}

// Same response shape, different data types
const userResponse: ApiResponse<User> = {
  data: { id: 1, name: "Alice" },
  status: 200,
  message: "OK"
};

const postResponse: ApiResponse<Post> = {
  data: { id: 1, title: "Hello World" },
  status: 200,
  message: "OK"
};

Key Takeaways

  • Generics = reusable code with type safety (instead of any)
  • <T> is a type variable — gets replaced with the actual type
  • T extends Something constrains what types are allowed
  • Common in APIs, data stores, and utility functions
  • TypeScript often infers the type — you don't always need <number> explicitly