TSTypeScript · Lesson 6 of 10

Generics

Generics let you write code that works with many types while staying type-safe. Think of `<T>` as a type parameter — like a function parameter, but for types.

TypeScript
// Generic function — works with any type T
function identity<T>(value: T): T {
  return value
}

const n = identity(42)          // T inferred as number
const s = identity("hello")     // T inferred as string
const explicit = identity<boolean>(true)

// Generic with constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user = { name: "Alice", age: 30, active: true }
const name = getProperty(user, "name")   // TypeScript knows this is string
const age  = getProperty(user, "age")    // TypeScript knows this is number
// getProperty(user, "email")   // Error: "email" not in user

// Generic class
class Stack<T> {
  private items: T[] = []

  push(item: T): void   { this.items.push(item) }
  pop(): T | undefined  { return this.items.pop() }
  peek(): T | undefined { return this.items.at(-1) }
  get size(): number    { return this.items.length }
  isEmpty(): boolean    { return this.items.length === 0 }
}

const numStack = new Stack<number>()
numStack.push(1)
numStack.push(2)
console.log(numStack.pop())   // 2

const strStack = new Stack<string>()
strStack.push("hello")
// strStack.push(42)   // Error: number not assignable to string
TypeScript
// Multiple type parameters
function zip<T, U>(arr1: T[], arr2: U[]): [T, U][] {
  return arr1.map((item, i) => [item, arr2[i]])
}

const pairs = zip([1, 2, 3], ["a", "b", "c"])
// pairs is [number, string][]

// Generic constraints
interface HasLength {
  length: number
}

function longest<T extends HasLength>(a: T, b: T): T {
  return a.length >= b.length ? a : b
}

console.log(longest("hello", "hi"))          // "hello"
console.log(longest([1, 2, 3], [1, 2]))      // [1, 2, 3]
// console.log(longest(1, 2))  // Error: number has no length

// Generic interfaces
interface Repository<T extends { id: number }> {
  findById(id: number): T | undefined
  findAll(): T[]
  save(item: T): void
  delete(id: number): void
}

// Generic type aliases
type Nullable<T>  = T | null
type Optional<T>  = T | undefined
type Result<T, E extends Error = Error> =
  | { ok: true;  value: T }
  | { ok: false; error: E }

function divide(a: number, b: number): Result<number> {
  if (b === 0) return { ok: false, error: new Error("Division by zero") }
  return { ok: true, value: a / b }
}

const res = divide(10, 2)
if (res.ok) console.log(res.value)   // 5
✦ Tip
Start with `<T>` and add constraints (`T extends SomeType`) only when the compiler tells you a property doesn't exist. Over-constraining generics defeats the purpose. The goal is to capture the relationship between input and output types, not to enumerate every property.