Lesson 5 of 5

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

Looks safe, isn't
function process(data: any) {
  return data.value.toUpperCase();
}

process({ value: 42 });
// compiles fine — any means
// "trust me", not "checked"
// crashes at RUNTIME instead
The fix: type it properly
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

  • any is assignable to and from anything, with zero checking in either direction — it's the "just trust me" type.
  • unknown is the safer sibling: also accepts anything, but forces you to narrow it (with typeof/instanceof, as in the previous lesson) before you can use it for anything at all.
  • any is "contagious" — once a value is any, everything derived from it becomes any too, 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.

tsconfig.json — loose
{
  "compilerOptions": {
    "strict": false
  }
}
tsconfig.json — recommended
{
  "compilerOptions": {
    "strict": true
  }
}
With strict off
function greet(name) {
  return "Hi " + name;
}
// name silently becomes `any`
// no error, no warning
With strict on
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.

Library ships its own types
npm install some-modern-lib
// its package.json already
// points at a bundled .d.ts —
// nothing extra to install
Library is plain JS, untyped
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.

What you write (.ts)
interface User { name: string; age: number; }

function isAdult(user: User): boolean {
  return user.age >= 18;
}
What actually runs (.js)
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.

← Back to
Next →