These TypeScript Bugs Look Innocent—But Costed Me Hours
Shahrukh Anwar

Shahrukh Anwar @devshahrukh

About: Senior Full Stack Developer (Laravel, Vue.js, TypeScript) passionate about clean code, scalable architecture & real-world products. I share lessons, stack insights, and build in public.

Location:
Prayagraj, India
Joined:
Jan 29, 2025

These TypeScript Bugs Look Innocent—But Costed Me Hours

Publish Date: Jul 22
0 0

Hey folks! 👋
After 3 years of working with TypeScript in real-world codebases, I’ve noticed that some bugs don’t scream—they whisper. These are small quirks that don’t throw errors during compile time but cause confusion (or worse) at runtime.


🪓 1. Misusing in for Type Narrowing

type Dog = { bark: () => void };
type Cat = { meow: () => void };
type Animal = Cat | Dog;

function speak(animal: Animal) {
  if ('bark' in animal) {
    animal.bark();
  } else {
    animal.meow(); // ❌ Error if animal is `{ bark: undefined }`
  }
}
Enter fullscreen mode Exit fullscreen mode

👀 Problem: TypeScript checks only property existence, not whether it's undefined.

✅ Fix: Add a runtime check:

if ('bark' in animal && typeof animal.bark === 'function') {
  animal.bark();
}
Enter fullscreen mode Exit fullscreen mode

🧨 2. Over-trusting as assertions

const user = JSON.parse(localStorage.getItem('user')) as { name: string };

// TypeScript thinks this is OK. But...
console.log(user.name.toUpperCase()); // 💥 Runtime error if user is null
Enter fullscreen mode Exit fullscreen mode

✅ Fix: Always validate after parsing:

try {
  const userRaw = JSON.parse(localStorage.getItem('user') || '{}');

  if ('name' in userRaw && typeof userRaw.name === 'string') {
    console.log(userRaw.name.toUpperCase());
  }
} catch {
  console.log('Invalid JSON');
}
Enter fullscreen mode Exit fullscreen mode

🖋 3. Missing Exhaustive Checks in switch

type Status = 'pending' | 'success' | 'error';

function getStatusMessage(status: Status) {
  switch (status) {
    case 'pending':
      return 'Loading...';
    case 'success':
      return 'Done!';
  }
}
Enter fullscreen mode Exit fullscreen mode

🛑 Issue: What if someone adds a new status like 'cancelled'?
✅ Fix: Add an exhaustive check:

function getStatusMessage(status: Status): string {
  switch (status) {
    case 'pending':
      return 'Loading...';
    case 'success':
      return 'Done!';
    case 'error':
      return 'Something went wrong.';
    default:
      const _exhaustiveCheck: never = status;
      return _exhaustiveCheck;
  }
}
Enter fullscreen mode Exit fullscreen mode

📌 4. Use satisfies when you want type safety without losing specificity.

const config = {
  retryCount: 3,
  logLevel: 'debug',
} satisfies Record<string, string | number>;
Enter fullscreen mode Exit fullscreen mode

✅ Why it’s useful:

Ensures config matches the expected shape, but preserves the literal types ('debug', 3) instead of widening them to string | number.

🔁 Without satisfies:

const config: Record<string, string | number> = {
  retryCount: 3,
  logLevel: 'debug',
};
// `retryCount` becomes number, not 3 (literal)
Enter fullscreen mode Exit fullscreen mode

🚀 TL;DR

  • Don’t trust in blindly. Validate runtime types.
  • Avoid as unless you know what you’re doing.
  • Use never for safety in switch statements.
  • Use satisfies when you feel it right to place.

These small things have saved me hours of debugging, and I hope they help you too!


💬 What’s the sneakiest bug TypeScript didn’t catch for you? Drop it in the comments 👇

🔁 Feel free to share this if it helps someone!

Comments 0 total

    Add comment