Runtime Validation Instead of Compile-Time Checks
JSDoc gets some of the compiler's power back, but it's opt-in — someone has to write the
comments, turn on @ts-check, and keep both accurate. When you can't rely on any of
that (a file nobody's annotated yet, a function that takes data from outside your program
entirely), the fallback is the same one JavaScript developers have always used: check the value
yourself, by hand, at the moment you receive it, and refuse to proceed if it's wrong.
💡 The Bottom Line
Where TypeScript's compiler used to guarantee a function's inputs matched its signature
before your code ever ran, plain JavaScript only gives you that guarantee if you write it
yourself — with typeof/instanceof checks, sane default parameter
values, and an early throw the moment something doesn't look right. It's more
typing than an annotation, it runs on every call instead of once at compile time, and it only
covers exactly what you remembered to check — but it's the only net left once the compiler is
gone.
Guard clauses at the top of a function
The most common pattern: check each argument's type explicitly, and throw
immediately with a clear message if it's wrong, before any real logic runs. This turns a
confusing failure three lines later into an obvious one at the exact call site.
function applyDiscount(price, percent) {
if (typeof price !== "number") {
throw new TypeError(`price must be a number, got ${typeof price}`);
}
if (typeof percent !== "number") {
throw new TypeError(`percent must be a number, got ${typeof percent}`);
}
return price - (price * percent) / 100;
}
applyDiscount(100, "ten");
// throws immediately, at the call site:
// TypeError: percent must be a number, got string
This doesn't stop the string from being passed in — nothing can, at runtime, since JavaScript
has no compile step to reject it beforehand. What it does is convert a silent, delayed
NaN into a loud, immediate error that points at the actual problem, which is most of
what the TypeScript compiler was buying you in the first place: fast, specific feedback instead
of a mystery bug several calls downstream.
instanceof for class-shaped values
For values that should be instances of a particular class rather than a primitive, the
equivalent check is instanceof.
class NetworkError {
constructor(status) {
this.status = status;
}
}
function logError(err) {
if (!(err instanceof NetworkError)) {
throw new TypeError("logError expects a NetworkError instance");
}
console.log("HTTP", err.status);
}
Default parameter values as a cheap safety net
Default parameters don't validate anything, but they close off one common source of the
exact bug TypeScript used to catch — calling a function with too few arguments and getting
undefined silently threaded through the rest of the function.
function greet(name = "friend") {
return `Hi ${name}`;
}
greet();
// TypeScript: compile error, missing required argument
// Plain JS without a default: "Hi undefined"
// Plain JS with the default above: "Hi friend"
⚠️ Gotcha
A default parameter only kicks in when an argument is undefined or omitted
entirely — it does not kick in for null. greet(null)
still returns "Hi null", not "Hi friend", which is a common surprise
for anyone assuming a default parameter behaves like "any falsy/missing value gets replaced."
Where to draw the line
Writing a guard clause for every parameter of every function quickly becomes its own kind of
noise. In practice, the discipline that pays off is checking at boundaries —
where data enters your program from somewhere you don't control: function arguments on a
public/exported function, data parsed from an API response, values read from
localStorage or a form input. Internal functions called only by your own
already-validated code can usually trust their inputs, the same way TypeScript would have let
inference carry a checked value through several internal calls without re-annotating it each
time.
For anything beyond a handful of ad-hoc checks — validating a whole API response shape, for
example — most JavaScript codebases reach for a small validation library (Zod, Yup, and
PropTypes for React component props are common choices) rather than hand-writing dozens of
typeof checks. Those libraries are worth knowing exist, but the underlying idea is
exactly the guard-clause pattern above: check the shape at the boundary, throw or report a clear
error if it doesn't match, and only proceed once it does.
🔧 Try It Yourself
Take the applyDiscount function from the guard-clause example above and add a
third check: percent must be between 0 and 100
inclusive, throwing a RangeError otherwise. Call it with 150 and
confirm you get an immediate, specific error instead of a silently wrong discount.
🙋 There Are No Dumb Questions
Q: Doesn't writing all these manual checks basically mean I'm hand-rolling a worse
version of what TypeScript already did automatically?
A: Yes, honestly — that's the core trade-off of losing the compiler. Manual guard clauses only
run when the function is actually called (so a bug in a rarely-exercised code path can still
hide for a long time), only check what you remembered to write, and add a small amount of
runtime overhead on every call that TypeScript's checks never had, since those were erased
before the code ever ran. The upside is they work with zero tooling and catch the highest-risk
spots — boundaries — which is usually enough to prevent the worst failures, even if it's not
the same guarantee.
✅ Quick Check
Given function greet(name = "friend") { return `Hi ${name}`; }, what does
greet(null) return?
Next up: guard clauses protect individual function calls, but TypeScript's interface
and generics did more than that — they guaranteed whole object shapes and let containers stay
connected to what they held. Losing those is a bigger structural change than losing a single
parameter check.