The Point of TypeScript Is Catching Bugs, Not Pleasing the Compiler
It is easy to lose the plot with TypeScript — to spend an hour wrestling a generic into shape for a type that catches a bug that would never have happened. The goal is leverage: maximum real bugs caught for minimum type-wrangling. The best practices below all serve that, not type purity for its own sake.
Treat "any" as a Code Smell
Every any is a hole in your type safety — past that point, TypeScript stops helping you. When you are tempted to reach for it, reach for unknown instead and narrow it down with checks, or take the time to write the real type. Turn on the strict compiler flags from day one; retrofitting strictness onto a loose codebase later is far more painful than starting strict.
Let Inference Do the Work
You do not need to annotate everything. TypeScript infers types well, and over-annotating just adds noise and maintenance. Annotate function parameters, return types of public functions, and anything inference gets wrong — but let local variables and obvious cases infer themselves. Good TypeScript often has fewer explicit types than beginners expect, not more.
Model Your Domain with Types
- Use union types for states — a value that is "loading | success | error" should be a union, so the compiler forces you to handle each case.
- Make illegal states unrepresentable — design types so impossible combinations simply cannot be constructed, and a whole class of bugs disappears.
- Prefer specific types over broad ones — a string union of allowed values beats a bare
stringthat accepts anything.
Use the Built-in Utility Types
TypeScript ships with utilities like Partial, Pick, Omit, Record, and Required that derive new types from existing ones. Using them keeps your types DRY — when the source type changes, the derived types follow automatically. Reinventing these by hand is wasted effort and a future inconsistency.
Generics: Powerful, Easy to Overdo
Generics are essential for reusable, type-safe utilities, but they are also where TypeScript gets unreadable fast. The guideline: use a generic when a function genuinely works over many types and you want to preserve the relationship between input and output. If you are adding three type parameters to satisfy one call site, step back — there is usually a simpler shape.
Validate at the Boundaries
TypeScript types vanish at runtime. Data coming from APIs, forms, or databases is not actually typed just because you wrote an interface for it. Validate external data at the boundary with a runtime schema validator, then trust your types inside the app. This is the single biggest source of "but TypeScript said it was a string" bugs.
How Dharmsy Uses TypeScript
We run strict mode everywhere, validate all external data at the boundary, and keep types as a tool for catching bugs rather than an end in themselves. The result is code that refactors safely and onboards new developers quickly. If your TypeScript is full of any and runtime surprises, we can help you tighten it up.

