DEV Community

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

Posted 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)