πŸ“š Learning Hub
Β· 2 min read

TypeScript Alone Won't Save Your Codebase


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:

  1. TypeScript β€” compile-time type checking
  2. Zod/Valibot β€” runtime validation for external data (API responses, form inputs, env vars)
  3. ESLint β€” catch patterns TypeScript allows but are still bad (no-explicit-any, no-non-null-assertion)
  4. Tests β€” catch business logic errors that types can’t express
  5. Strict mode β€” enable strict: true in 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: What is TypeScript Β· Zod Cheat Sheet

πŸ“˜