Lesson 5 of 5

Common Gotchas

The last four lessons covered the big, structural losses — the compiler, editor support, shape guarantees. This lesson is about the smaller, sneakier ones: everyday JavaScript behaviors that TypeScript was quietly protecting you from without you necessarily noticing, which now misbehave silently instead of getting flagged. None of these are new JavaScript features — they've always worked this way — but TypeScript's checks meant you rarely had to think about them directly.

💡 The Bottom Line

Four everyday traps come back once the compiler stops watching: implicit type coercion in operators like + and -, the null/undefined distinction that strictNullChecks used to enforce, missing cases in a switch that a union type's exhaustiveness checking used to catch, and — tying back to lesson 1 — the general rule that a mistake in any of these now surfaces as a wrong value or a crash at runtime, on whatever code path happens to trigger it, instead of a red squiggle while you were still typing.

Implicit coercion: + versus almost everything else

+ is the one arithmetic-looking operator that also means string concatenation, and JavaScript decides which behavior to use based on the runtime types of its operands — not anything declared anywhere. Every other arithmetic operator (-, *, /) always coerces both sides toward numbers, so the same kind of mixed-type expression behaves completely differently depending on which operator you happened to use.

"5" + 3     // "53"   — + sees a string, concatenates
"5" - 3     // 2      — -  always coerces to number
"5" * "2"   // 10     — *  always coerces to number
"five" - 3  // NaN    — coercion to number fails silently
[] + []     // ""     — both sides coerce to strings, then concatenate
[] + {}     // "[object Object]"

TypeScript's checker would have flagged most of these at the string - number step, well before the confusing part-string-part-arithmetic result ever got computed. Without it, the only way to catch this class of bug is noticing the wrong output — or the NaN — after the fact.

⚠️ Gotcha

Values arriving from HTML form inputs, URL query parameters, and most environment variables are always strings, even when they look like numbers. "5" + 3 silently becoming "53" instead of 8 is one of the most common real bugs in exactly this situation — a form field's value gets added to a running total without first being passed through Number(...) or parseInt(...), and the "total" quietly becomes a longer and longer string instead of a sum.

null versus undefined, unsupervised

TypeScript's strictNullChecks made you explicitly account for null/undefined anywhere a type allowed them, and the two are treated as distinct, deliberate values in the type system. In plain JS, both still exist and still mean subtly different things by convention — undefined usually means "never set," null usually means "deliberately set to nothing" — but nothing enforces that convention, and nothing warns you when code only handles one of the two.

function getDisplayName(user) {
  return user.nickname || "Anonymous";
}

getDisplayName({ nickname: undefined }); // "Anonymous"
getDisplayName({ nickname: null });      // "Anonymous"
getDisplayName({ nickname: "" });        // "Anonymous" — probably not intended!
// "" is falsy too, so the || pattern conflates
// "never set" with "deliberately set to empty"

TypeScript wouldn't have caught the "" case either — that's a logic bug, not a type error — but it would have made you handle null and undefined as two distinct, type-checked possibilities rather than both silently falling into the same || fallback along with every other falsy value.

No exhaustiveness checking on switch

A TypeScript union type over a fixed set of cases ("admin" | "editor" | "viewer") lets the compiler prove a switch handles every possible value — miss a case, and it can flag the gap immediately, especially with a never-typed default branch. Plain JS has no union type to check against, so a missed case just silently falls through to whatever the default does (or does nothing, if there's no default at all).

function permissionsFor(role) {
  switch (role) {
    case "admin":
      return ["read", "write", "delete"];
    case "editor":
      return ["read", "write"];
    // "viewer" case forgotten entirely
    default:
      return [];
  }
}

permissionsFor("viewer");
// silently returns [] — no error, no warning,
// just a user with fewer permissions than intended

This is the single gotcha most directly tied to the loss of the type system itself: TypeScript wasn't checking "is this switch written correctly" in the abstract, it was checking "does this switch cover every member of a specific union type" — and without a union type to check against in the first place, that entire category of verification has nothing left to run against.

🔧 Try It Yourself

Write the permissionsFor function above, deliberately omit one role's case, and call it with that role. Confirm you get a wrong-but-silent result rather than any kind of error — then add a comment listing every valid role directly above the function as a manual substitute for what a union type + exhaustiveness check would have caught automatically.

🙋 There Are No Dumb Questions

Q: These all sound like "just be careful" advice — is there really no structural way to guard against them in plain JS?
A: Partially. Linters (ESLint with rules like eqeqeq or no-implicit-coercion) catch some of the coercion cases at write-time without a full type system. Explicit === null/=== undefined checks instead of a blanket || handle the null/undefined conflation deliberately. And a default branch that throws (default: throw new Error("unhandled role: " + role)) turns a missed switch case into a loud runtime failure instead of a silent wrong answer — not caught before running, but at least caught the first time that code path executes, rather than never. None of these fully replace compile-time exhaustiveness checking, but they meaningfully narrow the gap.

✅ Quick Check

What does "5" - 3 evaluate to in JavaScript, and why?


That's the full picture of what changes when TypeScript is gone — time for the recap, which ties the five lessons together and points at what to do next.

← Back to
Next →