Skip to main content
CodeOath
← All posts

TypeScript63 min total · 21 parts

TypeScript Fundamentals: Types, Interfaces, Generics, and Why It Catches Bugs Before Runtime

Part 11 of 21 · ~2 min

Generics In Depth

function firstMatch<T>(items: T[], predicate: (item: T) => boolean): T | undefined {
  return items.find(predicate);
}

firstMatch([8841, 8842, 8843], (id) => id > 8841); // T: number — result: number | undefined
firstMatch(["a", "b"], (s) => s === "b");          // T: string — result: string | undefined

The alternative to <T> here isn't really an alternative: it's either any[] in and any out, which throws away every guarantee the function could have offered, or a hand-copied version of firstMatch for every type you ever need it for. <T> lets one function serve every type while still keeping the promises specific to whichever type actually got used — call firstMatch([8841, 8842], (id) => id > 0)?.toFixed(2), and it compiles, because TypeScript inferred T as number for that particular call; try the same .toFixed(2) on the string-array call, and it's refused, because that call's T was string, and strings don't have .toFixed.

Functions aren't the only thing that can carry a type parameter — interfaces, type aliases, and classes all can too. The queue is a natural fit: it needs to hold Applicant objects, but "a line of things, first in first out" has nothing to do with applicants specifically.

class Queue<T> {
  private items: T[] = [];

  enqueue(item: T): void {
    this.items.push(item);
  }

  dequeue(): T | undefined {
    return this.items.shift();
  }

  get size(): number {
    return this.items.length;
  }
}

const reviewQueue = new Queue<Applicant>();
reviewQueue.enqueue({ id: 8841, name: "Priya Shah", email: "priya@example.com", stage: "screening" });
reviewQueue.enqueue("not an applicant");
// Error: Argument of type 'string' is not assignable to parameter of type 'Applicant'

Write Queue<T> once, and TypeScript enforces a different, correct rule for every particular instantiation of it — Queue<Applicant> won't take a bare string, Queue<string> wouldn't take an Applicant, and the class body itself never needs a single if to tell the two situations apart.

A type parameter list isn't capped at one, and later parameters are free to build on earlier ones:

function withScore<T, S>(applicant: T, scoreDetails: S): T & S {
  return { ...applicant, ...scoreDetails };
}

const scored = withScore({ id: 8841, name: "Priya Shah" }, { score: 87, scoredAt: "2026-09-14" });
// scored: { id: number; name: string } & { score: number; scoredAt: string }