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 21 of 21 · ~3 min

Common Mistakes Worth Remembering

Seven ways this specific codebase — or one shaped like it — actually breaks, mapped to the chapter that explains why:

  • A field with a typo compiles and ships anyway. Passed through a named variable instead of a fresh object literal, which quietly turns off excess-property checking. (Structural typing.)
  • Two calls that look identical behave completely differently, and only one throws. An overload's return type genuinely depends on which signature matched — check which one actually fired before assuming the bug is elsewhere. (Functions, overloads, and this.)
  • A new status value falls through into the wrong branch, silently. A discriminated union grew a variant and the switch handling it wasn't updated — assertNever would have refused to compile the moment that happened. (Discriminated unions.)
  • A value that's obviously correct in the source still gets rejected by the compiler. A literal that widened to its general type the instant it became a property of a plain object. as const is the fix. (Literal types.)
  • err.message works in testing and then explodes on a real error in production. The catch variable defaults to any; only strict (or useUnknownInCatchVariables specifically) makes it unknown and forces a check first. (unknown, any, and never.)
  • A cast makes an error message disappear instead of the actual bug. as unknown as X forces past the one check that would have caught the mismatch — exactly the spot a real mistake goes unreported rather than flagged. (Type assertions.)
  • A reviewer's inbox goes quiet with nothing in any log to explain it. An Applicant | undefined read without a check. This is the bug that opened this whole reference, and strictNullChecks is the only thing on this list that actually prevents it rather than just catching it faster. (tsconfig.json.)

Here's the queue as it actually exists by the end of this reference — every mechanism above, compressed into about a dozen lines:

class ApplicationQueue {
  private items: Applicant[] = [];
  constructor(readonly queueId: string) {}

  enqueue(applicant: Applicant): void { this.items.push(applicant); }
  dequeue(): Applicant | undefined { return this.items.shift(); }
  get size(): number { return this.items.length; }
}

function notifyReviewer(queue: ApplicationQueue, reviewer: Reviewer): void {
  const next = queue.dequeue();
  if (!next) return;                                    // required, once strictNullChecks is on
  sendEmail(reviewer.email, `Review ${next.name}`, `/applicants/${next.id}`);
}

The same bug, somewhere else entirely

Building everything around one running program is how the ideas actually stick — but it leaves open a real question: did the mechanism stick, or just this particular story? The way to check is to look for the same shape somewhere that shares nothing with the queue at all. Here's the exact bug that opened this reference, in a script with no applicants, no reviewers, and nothing to do with hiring — a shipping-rate lookup by zip code:

const rates = new Map<string, number>();
rates.set("94103", 4.99);

function getRate(zip: string): number {
  const rate = rates.get(zip); // Map.get() always returns V | undefined, for every key, every time
  return rate * 1.08;          // Error under strictNullChecks: 'rate' is possibly 'undefined'
}

Map.get() hands back V | undefined for the identical reason dequeue() does: there's no value to return for a key — or a queue — that doesn't have one. No applicant record anywhere near this function, and the bug is unmistakably the same one. If that's visible on sight, the lesson survived past the specific story it was taught through, which was the entire reason to tell only one story in the first place.

None of this exists in isolation from the rest of the stack. React Fundamentals is the component-and-hooks side of what a typed frontend actually looks like day to day, and JavaScript Core Concepts is the runtime underneath all of it — scope, closures, this, the event loop — none of which TypeScript changes even slightly; it only ever describes it more precisely. Reading about narrowing, generics, and the erasure gotchas gets you partway there. Actually watching one of these compile errors fire, on code you wrote, is what makes it stick — the code lab has exercises built for exactly that.