Lesson 6 of 13 · 0 of 1 exercises solved

Generics

let one or work with many types while staying type-safe. You will learn to read and write type like `<T>`.

// 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 (compiler loads on first run)Ctrl+Enter runs
// 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
TypeScript (compiler loads on first run)Ctrl+Enter runs

Common question

Do I need generics when any exists?

A generic is a function, class or type that takes types as parameters, so one definition works for many types without losing precision. <T> is a stack of whatever the caller picks, and the compiler tracks that choice through every push and pop. Constraints (T extends Something) let the generic rely on what T can do while staying open.

Reach for a type parameter whenever a value goes in and a related value comes out. Give parameters meaningful names when there are several, constrain them when you need a property, and let inference pick T at the call site so callers rarely write the angle brackets themselves.

Source: <T>, <T> and Map<K, V> are all generic. If you use them, you already rely on generics every day.

Common mix-ups

Each of these sounds reasonable. Think about why it is wrong, then open the explanation.

  • A function that takes any handles every type, which is all generics do.

  • A generic type parameter is a runtime value the function can inspect.

  • Generics are for authors and too abstract for app code.

Practice

Write a generic function firstOr<T>(items: T[], fallback: T): T that returns the first element or the fallback, and a generic class Stack<T> with push, pop (returns T | ) and a size getter.

Exercise

Generic function and class

3 checks
function firstOr(items, fallback) {
  return fallback
}

class Stack {
  items = []
}

console.log(firstOr([3, 4], 0), firstOr([], "none"))
const s = new Stack()
s.push("a"); s.push("b")
console.log(s.pop(), s.size)
 
TypeScript (compiler loads on first run)Ctrl+Enter runs
Output
Press Run to try your code, then Check answer to grade it.
Finished reading?1 exercise still open. You can come back to it later.

Tip: use the ← and → keys to move between lessons.