Common Gotchas
Everything so far made TypeScript sound like it always catches the bug for you. It doesn't — there are several well-known escape hatches and edge cases where TypeScript's safety net has a hole in it, usually because the checking was opted out of, not because it failed. Knowing where those holes are is what separates "I added type annotations" from "I actually get the safety TypeScript promises."
💡 The Bottom Line
The biggest gap is any — a type that turns off checking entirely for whatever
it's attached to, and it spreads silently to anything that value touches. Turn on
strict mode to make TypeScript refuse to let any sneak in
unannounced, use @types packages to get type information for untyped JS
libraries, and always remember that none of this exists once your code is actually running —
types are erased at compile time, not enforced at runtime.
any: the type that disables the type system
function process(data: any) {
return data.value.toUpperCase();
}
process({ value: 42 });
// compiles fine — any means
// "trust me", not "checked"
// crashes at RUNTIME instead
interface Data {
value: string;
}
function process(data: Data) {
return data.value.toUpperCase();
}
process({ value: 42 });
// compile error: 42 is not
// assignable to type 'string'
📋 Key differences
anyis assignable to and from anything, with zero checking in either direction — it's the "just trust me" type.unknownis the safer sibling: also accepts anything, but forces you to narrow it (withtypeof/instanceof, as in the previous lesson) before you can use it for anything at all.anyis "contagious" — once a value isany, everything derived from it becomesanytoo, silently, unless you stop it with an explicit annotation somewhere downstream.
strict mode: turning the safety net all the way on
By default, plenty of unsafe patterns compile without complaint — an unannotated function
parameter quietly becomes any, and a variable can be null even where
its type says it can't. The strict compiler option (in tsconfig.json)
turns on a bundle of checks that close these gaps, most notably noImplicitAny and
strictNullChecks.
{
"compilerOptions": {
"strict": false
}
}
{
"compilerOptions": {
"strict": true
}
}
function greet(name) {
return "Hi " + name;
}
// name silently becomes `any`
// no error, no warning
function greet(name) {
return "Hi " + name;
}
// compile error: Parameter 'name'
// implicitly has an 'any' type
⚠️ Gotcha
A brand-new project scaffolded by most modern tooling (Vite, Next.js, etc.) already has
strict: true — but plenty of tutorials, older boilerplates, and hand-rolled
tsconfig.json files still default to false. If TypeScript is
letting through code you were sure it should catch — especially unannotated parameters or
null/undefined access — the very first thing to check is whether
strict is actually on.
Untyped third-party JavaScript libraries
Not every package on npm is written in TypeScript, and yet you still want type checking and
autocomplete when you use it. TypeScript solves this with separate declaration
files (.d.ts) that describe a library's shape without containing any
actual implementation — often published separately as an @types/ package.
npm install some-modern-lib
// its package.json already
// points at a bundled .d.ts —
// nothing extra to install
npm install lodash
npm install --save-dev @types/lodash
// @types/lodash supplies the
// declaration file separately
If no @types package exists at all for a library, importing it will either
error with "could not find a declaration file" or fall back to any for everything
it exports — which quietly reintroduces the exact hole described above. The fix, if you're
stuck without official types, is to write a small ambient declaration yourself
(declare module "library-name";) as a stopgap.
Type erasure: types don't exist at runtime
This closes the loop back to lesson one, and it's the single most important mental model to
hold onto: TypeScript's entire type system is a compile-time overlay. Once tsc
finishes checking your code, every type annotation, interface, and generic parameter is deleted
from the emitted JavaScript — none of it can be inspected, checked, or relied on while the
program is actually running.
interface User { name: string; age: number; }
function isAdult(user: User): boolean {
return user.age >= 18;
}
function isAdult(user) {
return user.age >= 18;
}
// interface User is gone entirely —
// it never existed at runtime
⚠️ Gotcha
Because types are erased, you cannot use typeof or instanceof to
check against an interface or type alias at runtime — only against
real JavaScript constructs that still exist after compilation, like classes, or values whose
actual runtime type (string, number, object) you're checking. Data arriving from outside your
program's control — an API response, JSON.parse, user input — has whatever shape
it actually has, regardless of what type you annotated it with; TypeScript checked your code
assuming that shape, but nothing enforces it at the boundary unless you add runtime
validation (e.g. a schema library) there yourself.
🔧 Try It Yourself
Take a small function typed with an any parameter, compile it, and read the
emitted .js output — confirm the annotation is simply gone. Then flip
any to a proper interface, re-run tsc with
"strict": true in tsconfig.json, and pass it a value with one wrong
field to see the compiler catch what any was letting through silently.
🙋 There Are No Dumb Questions
Q: If types are erased and don't exist at runtime, can a malformed API response
still crash my "safely typed" code?
A: Yes — and this is the gap developers new to TypeScript most often miss. An
interface Response { status: number } annotation only tells the compiler "assume
this is the shape," it doesn't verify the actual JSON that arrives over the network matches
it. TypeScript protects you from mistakes in your own code; it does nothing to validate
external data unless you add a runtime check (a schema library like Zod, or a manual
shape-check) at the point that data enters your program.
✅ Quick Check
Your tsconfig.json has "strict": false, and you write
function add(a, b) { return a + b; } with no annotations. What happens?
That's the core of the transition — you now know how JavaScript's dynamic instincts map onto TypeScript's compile-time checks, and where the checks quietly stop working. The recap page ties it together and points at where to go next.