·6 min read

Pragmatic TypeScript: type the boundaries, trust the inference

Five years of TypeScript and one strong opinion: type the system boundaries, skip the generic gymnastics, never reach for any when unknown will do.

TypeScriptPatterns

I have written TypeScript professionally for five years and the opinion I hold most strongly is also the most unfashionable: most frontend codebases use TypeScript wrong. They are either undertyped (everything is any at the seams, the type checker is ornamental) or overtyped (generic constraints with three type parameters describing a state machine for a button). Both produce bugs. The middle ground is narrower than people think.

Two places that justify types

First: every system boundary. API responses, form inputs, URL parameters, third-party callbacks, anything crossing into your code from somewhere you do not control. These are the places where wrong assumptions become production bugs. Use Zod (or Valibot, or ArkType) to parse at the boundary. Once parsed, the rest of the code can rely on the static type without runtime checks.

Second: domain types that show up in many places. A User, an Order, a CartLine. Define them once, import them everywhere, and let renames propagate through the codebase automatically. This is the original TypeScript value proposition and it is still the strongest one.

Two places types waste time

Internal helper functions where the inferred type is fine. Writing function add(a: number, b: number): number { return a + b} when TypeScript would infer the return type by itself adds a maintenance liability without any safety win. Let the inference engine do its job.

Generic component props that abstract over something you never reuse. I have seen <Table<T, K extends keyof T>> components used once in the codebase. Just write the concrete component. The next time you need a table, write a second one. By the third you will know what the right abstraction is. Two concrete implementations are easier to read and refactor than one premature abstraction.

The any escape hatch

Use unknown, not any. The difference is that unknown forces a check before use, while any propagates silently and kills the type system across whatever it touches. If you cannot type something, type it as unknown and narrow it at the use site.

The only legitimate any is at a boundary you are about to remove (a half-migrated module, a temporarily-untyped library). Leave a TODO with a date. If the TODO is still there six months later, it was not temporary.

Strict mode is non-negotiable

Turn on every flag in strict, plus noUncheckedIndexedAccess and exactOptionalPropertyTypes. These two are the ones that catch the bugs you would actually ship without them. Indexed access returning T | undefined instead of T reveals an entire class of crashes that loose mode hides.

The combined cost is a few extra null checks. The benefit is that arr[i].name never throws in production. That trade is always worth it.

The summary I would put on a sticker

Type the boundaries. Trust the inference inside. Use unknown, never any. Generics are a tool, not a goal. The TypeScript that helps you is the TypeScript that disappears into the background, not the TypeScript that becomes a second language to learn.