TypeScript Best Practices and Common Mistakes

advanced
13 min

TypeScript Best Practices and Common Mistakes

You now know the TypeScript language. This final lesson is about using it well: the habits that make types an asset instead of noise, and the mistakes that quietly erode the safety you adopted TypeScript for. Treat this as a checklist to revisit as your projects grow.

Turn On Strict Mode — and Keep It On

Everything in this series assumes "strict": true. Without it, null checks vanish and implicit any spreads through your code. If you inherit a non-strict codebase, enable flags incrementally (noImplicitAny first, then strictNullChecks) rather than living without them. Add noUncheckedIndexedAccess for arrays and dictionaries once the team is comfortable.

Mistake 1: Reaching for any

any silently disables checking for a value and everything derived from it. Nearly every use has a better alternative:

typescript
// Bad: nothing is checked function handle(payload: any) { return payload.user.name; } // Good: unknown forces validation function handleSafe(payload: unknown) { if ( typeof payload === "object" && payload !== null && "user" in payload ) { // narrowed step by step, or better: validate with a schema library } }

When any is genuinely unavoidable (untyped third-party code), quarantine it in one small wrapper function with a typed signature so it cannot leak.

Mistake 2: Overusing Type Assertions

as tells the compiler "trust me" — it changes nothing at runtime and can be flatly wrong:

typescript
const data = JSON.parse(raw) as UserProfile; // hope-driven development

If the JSON does not match UserProfile, you get runtime crashes with a green build. Prefer narrowing, custom type guards, or schema validation (Zod, Valibot) at every boundary where data enters your program: HTTP responses, file reads, message queues, localStorage. Inside those boundaries, honest types flow without assertions. Reserve non-null ! for cases you can prove, and consider a thrown error with a clear message instead.

Mistake 3: Typing What Inference Already Knows

Redundant annotations add noise and can even mask mistakes:

typescript
// Noisy const names: string[] = ["Ada", "Grace"].map((n: string): string => n.trim()); // Clean — identical types, all inferred const names2 = ["Ada", "Grace"].map(n => n.trim());

Annotate where types are contracts: exported function signatures, public APIs, and empty initializations. Let inference handle locals and callbacks.

Mistake 4: Duplicating Types Instead of Deriving Them

When two hand-written types describe overlapping data, they will drift apart. Derive instead:

typescript
interface User { id: number; name: string; email: string } type UserUpdate = Partial<Omit<User, "id">>; // derived, stays in sync type UserRow = User & { createdAt: Date }; const ROLES = ["admin", "member"] as const; type Role = (typeof ROLES)[number]; // one source of truth

Utility types, keyof, typeof, and as const exist precisely so a change in one place propagates everywhere.

Mistake 5: Stringly-Typed and Boolean-Flag APIs

Precise types prevent invalid states from being representable:

typescript
// Weak: any string accepted, flags can contradict each other function fetchData(url: string, isLoading: boolean, hasError: boolean) {} // Strong: a discriminated union — impossible states are impossible type RequestState = | { status: "idle" } | { status: "loading" } | { status: "success"; data: string[] } | { status: "error"; message: string };

A switch on status with a never exhaustiveness check then guarantees every state is handled — and keeps guaranteeing it as states are added.

Everyday Habits That Compound

  • Accept readonly T[] parameters; return concrete types.
  • Prefer literal unions over enums unless you need nominal behavior.
  • Run tsc --noEmit in CI so type errors block merges even when a bundler builds the app.
  • Do not suppress errors with @ts-ignore; if you must, use @ts-expect-error with a comment — it errors when the underlying problem is fixed.
  • Keep dependencies' types updated; mismatched @types versions cause phantom errors.

Key Takeaways

  • Strict mode is the foundation; add noUncheckedIndexedAccess when ready.
  • Replace any with unknown plus narrowing; validate all external data at the boundary.
  • Assertions bypass checking — treat every as as a code smell to justify.
  • Derive related types with utilities instead of duplicating them.
  • Model state with discriminated unions so invalid combinations cannot exist.

Congratulations — you have completed the CodeYourCraft TypeScript series! Revisit any lesson as a reference, and start applying these patterns in your next project.

TypeScript Best Practices and Common Mistakes - TypeScript | CodeYourCraft | CodeYourCraft