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 }` } }
👀 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(); }
🧨 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
✅ 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'); }
🖋 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!'; } }
🛑 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; } }
📌 4. Use satisfies
when you want type safety without losing specificity.
const config = { retryCount: 3, logLevel: 'debug', } satisfies Record<string, string | number>;
✅ 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)
🚀 TL;DR
- Don’t trust
in
blindly. Validate runtime types. - Avoid
as
unless you know what you’re doing. - Use
never
for safety inswitch
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)