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!