TypeScript is the best thing to happen to JavaScript. But I keep seeing teams adopt it and think their code quality problems are solved. Theyβre not.
What TypeScript catches
- Passing a string where a number is expected
- Accessing properties that donβt exist
- Missing function arguments
- Wrong return types
This is genuinely valuable. No argument there.
What TypeScript doesnβt catch
1. Runtime data from external sources
interface User {
name: string;
email: string;
}
// TypeScript says this is a User. But is it?
const user: User = await fetch("/api/user").then(r => r.json());
TypeScript trusts you. If the API returns { name: null, email: 42 }, TypeScript wonβt complain. Your app will crash at runtime.
Fix: Use Zod or Valibot to validate external data:
const UserSchema = z.object({
name: z.string(),
email: z.string().email(),
});
const user = UserSchema.parse(await fetch("/api/user").then(r => r.json()));
// Now it's actually validated, not just typed
2. Business logic errors
function transferMoney(from: Account, to: Account, amount: number): void {
from.balance -= amount;
to.balance += amount;
}
TypeScript is happy. But what if amount is negative? What if from.balance is insufficient? Types donβt encode business rules.
3. The any escape hatch
Every codebase has them. any turns off TypeScript for that value. One any in a function signature infects everything downstream.
function processData(data: any) {
// TypeScript has left the chat
return data.foo.bar.baz; // No error. Will crash if structure is wrong.
}
4. Type assertions lie
const element = document.getElementById("app") as HTMLDivElement;
// What if it's null? What if it's a span? TypeScript doesn't check.
as is you telling TypeScript βtrust me.β TypeScript trusts you. The runtime doesnβt.
The full safety stack
TypeScript is layer 1. You need more layers:
- TypeScript β compile-time type checking
- Zod/Valibot β runtime validation for external data (API responses, form inputs, env vars)
- ESLint β catch patterns TypeScript allows but are still bad (
no-explicit-any,no-non-null-assertion) - Tests β catch business logic errors that types canβt express
- Strict mode β enable
strict: truein tsconfig (many projects donβt)
The tsconfig settings most people miss
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"exactOptionalPropertyTypes": true
}
}
noUncheckedIndexedAccess alone catches a huge class of bugs β accessing array elements or object properties that might not exist.
The takeaway
TypeScript is necessary but not sufficient. Itβs a seatbelt, not a self-driving car. You still need to steer.
The teams with the fewest production bugs arenβt the ones with the fanciest type system. Theyβre the ones that combine TypeScript with runtime validation at boundaries, integration tests for critical paths, and strict tsconfig settings that catch the bugs TypeScript normally misses.
Start with strict: true and noUncheckedIndexedAccess: true. Add Zod to your API boundaries. Write integration tests for your happy paths. That combination catches more bugs than any amount of generic type gymnastics.
Related resources
Related: What is TypeScript Β· Zod Cheat Sheet