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.