50 TypeScript F*ck Ups (I’ve Made So You Don’t Have To)

Prashant Adhikari
Nov 11, 20258 min read
TypeScript promises safety.
In practice, it gives you a false sense of it — until you learn how to actually use it.
Over the years, I’ve made every possible TypeScript mistake: from silent runtime bugs to type gymnastics gone wrong.
This is a survival guide — 50 TypeScript f*ck-ups, explained so you don’t repeat them.
TypeScript isn’t just “JavaScript with types.”
It’s a language layered on top of another language.
That means:
It doesn’t change runtime behavior.
It only changes how you think about code.
If you treat TypeScript like a stricter linter, it’ll fight you.
Treat it like a modeling tool, and suddenly, it starts working for you.
any because “it’s faster”any is TypeScript’s “whatever” button. It feels good until your types are meaningless.
function add(a: any, b: any) {
return a + b;
}
add("5", 5); // "55" 🙃✅ Takeaway: Use unknown or proper typing. any is just JavaScript with more steps.
If you’re seeing red squiggles, TypeScript is literally trying to save you.
✅ Takeaway: Treat warnings as bugs. Warnings today = production issues tomorrow.
strict modeHalf the bugs in beginner TypeScript projects exist because someone forgot "strict": true.
✅ Takeaway: Add "strict": true to your tsconfig.json and never look back.
undefined and nullThey’re not the same. undefined = not assigned. null = intentionally empty.
Mixing them makes your type guards cry.
✅ Takeaway: Pick one convention and stick to it. Prefer undefined in modern code.
! (non-null assertion)You’re basically saying, “Trust me, TypeScript, it’s there.” Spoiler: it’s not.
const name = maybeName!;
console.log(name.length); // 💥 runtime error✅ Takeaway: Use type narrowing instead. Don’t silence your compiler like it’s your mom.
??)Stop using || when 0 or "" are valid values.
const count = input || 10; // breaks if input = 0
const fixed = input ?? 10; // ✅ correct✅ Takeaway: Use ?? when you mean nullish, not falsy.
var in 2025If you’re still using var, I hope you’re also using IE6.
✅ Takeaway: Use let or const. Scoping matters, and you’ll save yourself debugging pain.
as)as is great until you start lying to the compiler.
const user = {} as User; // You just promised a lie.✅ Takeaway: Assert only after validation, not imagination.
neverIf a switch statement or function “can’t happen,” prove it.
function handle(x: "a" | "b") {
switch (x) {
case "a": break;
case "b": break;
default: const _exhaustive: never = x;
}
}✅ Takeaway: never makes your compiler your QA engineer.
object instead of proper typesobject means “anything not primitive.” Not helpful.
✅ Takeaway: Define real shapes. Record<string, unknown> or an interface beats object every time.
type and interfaceThey’re 90% similar — the other 10% breaks everything.
✅ Takeaway:
Use interface for objects you’ll extend.
Use type for unions, primitives, and utilities.
type Result = Success | Failure | NetworkError | Timeout | Sadness
You’re writing a novella, not a type.
✅ Takeaway: Keep unions lean. Simplify before your brain melts.
If you’re checking for properties manually, you’re doing extra work.
type Response = { kind: "ok"; data: string } | { kind: "error"; msg: string };
function handle(res: Response) {
if (res.kind === "ok") console.log(res.data);
}✅ Takeaway: Use a kind field and let TS narrow for you.
Why type status: string when you can do:
type Status = "loading" | "done" | "error";✅ Takeaway: Be specific. string is too vague for real state machines.
Type guards are how you make TypeScript smart.
function isUser(u: any): u is User {
return u && typeof u.name === "string";
}✅ Takeaway: Guard early, fail fast.
keyofIf you find yourself typing property names twice, keyof exists for a reason.
✅ Takeaway: Automate key access — let TypeScript handle the strings.
Why write this:
type PartialUser = { name?: string; age?: number };When you can just do:
type PartialUser = Partial<User>;✅ Takeaway: Partial, Pick, Omit, Record, and ReturnType are your best friends.
PickSometimes you Pick so hard, you end up missing context.
✅ Takeaway: If you’re picking 5+ fields, maybe just define a new type.
ReadonlyImmutability is your friend. Especially with shared state.
✅ Takeaway: Mark what shouldn’t change. Save your future self from tears.
When you’re at Promise<Array<Record<string, Map<string, User[]>>>>
…it’s time to stop.
✅ Takeaway: Break types into named pieces. Your eyes (and coworkers) will thank you.
function get<T>(arr: T[], index: number) { return arr[index]; }✅ Takeaway: Add constraints! T extends object or T extends keyof U keeps your types honest.
Don’t over-declare generics when TS can infer them.
✅ Takeaway: If TypeScript can figure it out — let it.
<T = any>Defaulting to any defeats the purpose.
Join Medium for free to get updates from this writer.
Subscribe
✅ Takeaway: Default to something meaningful, not chaos.
type Response<T> = { data: T };
function handle<T>(res: Response<T>) {} // same name, new headache✅ Takeaway: Don’t reuse generic names inside functions. TS hates that.
You can infer return types automatically. Stop over-specifying.
✅ Takeaway: Let TypeScript infer return types unless clarity demands otherwise.
Optional means “may be undefined,” not “don’t care.”
✅ Takeaway: Handle undefined before it handles you.
Function as a typeThat’s like labeling your variable “thing”.
✅ Takeaway: Use (args: Args) => Return signatures. Always.
Sometimes one function can have multiple signatures cleanly.
✅ Takeaway: Use overloads instead of union spaghetti.
voidReturn types matter. If you ignore void, you’ll chain undefined.
✅ Takeaway: Be explicit with void when functions only do, not return.
An async function always returns a Promise. Always.
✅ Takeaway: Don’t double-wrap. Promise<Promise<T>> is a cry for help.
class User {
name: string; // 💥 strict mode hates this
}✅ Takeaway: Initialize or use definite assignment (!), but do it responsibly.
Not every field needs privacy; sometimes you just want immutability.
✅ Takeaway: Use readonly when it’s about protection, not secrecy.
implementsYour classes are lying if they don’t implements the interface they pretend to follow.
✅ Takeaway: Explicit > implicit.
You “extend” the class but “break” the contract.
✅ Takeaway: Don’t weaken parent types — extend responsibly.
You can’t await new. Ever.
✅ Takeaway: Use a static create() method instead.
this contextArrow functions exist to fix your binding crimes.
✅ Takeaway: Prefer arrow functions in callbacks or event handlers.
If you’re storing dynamic keys, define them.
interface Dictionary {
[key: string]: string;
}✅ Takeaway: Index signatures are sanity savers.
Extending Array or Error needs super() or things break mysteriously.
✅ Takeaway: When in doubt, call super().
awaitHalf of TypeScript bugs are just missing await.
✅ Takeaway: Await everything that’s async. You’re not saving time; you’re losing sanity.
Try/catch exists for a reason. So does .catch().
✅ Takeaway: Unhandled rejections are not “fine.”
Enums are overkill for static strings.
✅ Takeaway: Prefer as const objects — smaller, simpler, faster.
unknown and anyunknown forces you to prove safety. any trusts you blindly.
✅ Takeaway: unknown > any when in doubt.
If you don’t narrow types, you’re just guessing.
✅ Takeaway: Use in, typeof, and instanceof — TS loves that.
await inside a loop is sequential. You wanted Promise.all().
✅ Takeaway: Await smartly, not blindly.
readonly in arraysconst arr: readonly number[] = [1, 2, 3];
arr.push(4); // nope✅ Takeaway: Use readonly arrays to prevent mutation bugs.
forEach doesn’t await. It just runs and leaves.
✅ Takeaway: Use for...of with await, not forEach.
You imported the type but forgot to export it. Welcome to refactor hell.
✅ Takeaway: Export your types deliberately.
as constIt locks your literals and makes your types smarter.
const roles = ["admin", "user"] as const;
type Role = typeof roles[number];✅ Takeaway: as const turns data into exact types.
TypeScript types disappear at runtime. Don’t expect them to save you in if.
✅ Takeaway: Types don’t exist after compile. Validate runtime inputs manually.
TypeScript gives you types, not superpowers.
Bad logic with great types is still bad logic.
✅ Takeaway: TypeScript is a tool, not a babysitter.
You don’t learn TypeScript by reading the docs — you learn it by breaking things and fixing them.
I’ve done most of the breaking for you.
Use types as guardrails, not handcuffs.
Your future self (and your team) will thank you.

Written by
Prashant Adhikari
Full-stack engineer writing about the things I build, break and eventually fix.
CaloGlow - Only Calories Tracker You Need
Jul 27, 2026