DEV Community

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

Posted on Edited on

These TypeScript Bugs Look Innocent—But Costed Me Hours

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!

Top comments (0)