Common TypeScript bugs and gotchas — a field reference
Every bug in this lesson has already been explained mechanically somewhere earlier in this domain — this is the field-reference version, the shape each one actually takes in real code, so it's recognizable on sight instead of requiring the mechanism to be re-derived from scratch every time.
4 min read
Bug 1: any silently disabling type-checking for everything it touches
function processData(data: any) {
return data.valeu.toUpperCase(); // TYPO ("valeu") — compiles fine, any catches NOTHING
}Covered mechanically in the any/unknown/assertions lesson: any opts an entire value out of type-checking, so a typo'd property access, a wrong method call, anything at all compiles without complaint. The fix: unknown for genuinely uncertain values, forcing a real narrowing check before use — or better, a specific type describing the actual expected shape.
Bug 2: an object literal widened by an explicit annotation, losing specific type information
const config: Record<string, number | string> = { retries: 3 };
config.retries.toFixed(2); // TYPE ERROR — TypeScript only knows `number | string`, not specifically numberCovered mechanically in the satisfies-operator lesson: an explicit annotation replaces the literal's own more specific inferred type with the broader annotated type. The fix: satisfies instead of a plain annotation, when the goal is validation without losing the value's own precise inferred type.
Bug 3: a union of object shapes with no field designed for narrowing
type Shape = { radius: number } | { sideLength: number };
function area(s: Shape) {
if ("radius" in s) { /* ... */ } // fragile — checking an incidental property, not a designed discriminant
}Covered mechanically in the discriminated-unions lesson: without a shared, literal-typed field, narrowing a union of object shapes relies on checking incidental property presence rather than something the types were actually designed to be checked by. The fix: add a kind/type discriminant field to every union member, and narrow with a switch on it.
Bug 4: a forgotten case when a union grows, with no compiler warning
type Shape = Circle | Square | Triangle; // Triangle added LATER
function area(s: Shape) {
switch (s.kind) {
case "circle": /* ... */ break;
case "square": /* ... */ break;
// Triangle case FORGOTTEN — no error, silently falls through to nothing
}
}Covered mechanically in the discriminated-unions lesson: without exhaustiveness checking, adding a new union member doesn't force every existing switch to handle it — the omission compiles silently. The fix: a default case assigning to a never-typed variable, which becomes a real compile error the moment any case is left unhandled.
Bug 5: mistaking a decorator's timing — expecting per-instance behavior from a class decorator
function announce(target: Function) {
console.log("Widget instance created!"); // WRONG assumption — this actually logs at CLASS DEFINITION
}
@announce
class Widget {}
new Widget(); new Widget(); // "Widget instance created!" only logged ONCE, not twiceCovered mechanically in the decorators lesson: a decorator's own code runs once, when the class or method is defined, not each time an instance is created with new. The fix: for genuinely per-instance behavior, the logic needs to live inside the constructor or the wrapped method itself, not in the decorator function's own top-level body.
Bug 6: casting a third-party object property instead of augmenting its real type
app.get("/profile", (req: any, res) => { // loses type-checking for req ENTIRELY, everywhere it's used
console.log(req.usr?.id); // TYPO — silently compiles, `any` catches nothing
});Covered mechanically in the module-augmentation lesson: casting to any at every use site to work around a missing property loses type safety for the entire object, everywhere. The fix: declare module to augment the library's actual type definition once, giving every use site real, checked access to the added property.
The actual throughline across all six
Every one of these traces back to the same handful of mechanisms this domain already covered in depth: any and casting both opt out of the type system rather than working with it, and the compiler's real guarantees (discriminated-union narrowing, exhaustiveness checking, declaration merging) only protect the code that actually uses them deliberately. Recognizing a bug's shape on sight — "this smells like a widened annotation," "this smells like a missing discriminant" — is what separates fixing a TypeScript issue quickly from re-deriving these mechanisms from first principles every single time one shows up.
Further reading
Check your understanding
A quick comprehension check — not tracked, not graded, just for you.
1. Why does `data: any` allow a typo like `data.valeu` to compile without any error?
2. Why does a class decorator's console.log only fire once across multiple `new Widget()` calls, when someone might expect it to fire per instance?
3. What's the actual throughline connecting all six bugs in this field-reference lesson?