All Insights
technical· 19 min read

TypeScript Patterns for Production

Type-level programming that pays off

SV
Sri VardhanNovember 5, 2023
Share on Twitter
Share on LinkedIn
Copy link

Advanced TypeScript patterns that make your codebase more maintainable-discriminated unions, branded types, type guards, and more.

Types are leverage, not decoration

The teams that get the most out of TypeScript treat the type system as a design tool, not just a static checker. They use types to encode invariants, eliminate categories of bugs at compile time, and make refactors safe. The teams that get the least out of it sprinkle any like seasoning and write the same runtime checks they would in plain JavaScript.

This is not a tour of every advanced TypeScript trick. It is the small set of patterns I keep using on production codebases because they pay back the time spent learning them many times over.

Discriminated unions for state

The single highest-leverage pattern I know. Any time a value can be in one of several shapes, model it as a discriminated union with a kind or status field.

The compiler now refuses to let you read a value field from an error case or an error field from a success case. Whole categories of "I forgot to check that field" bugs disappear. I model API loading state, form validation, payment status, and most domain workflows this way.

When I onboard onto an existing codebase, the first refactor I usually suggest is replacing status strings paired with optional fields with a proper discriminated union. The bug count drops noticeably within a sprint.

Branded types for unit safety

A user ID and an order ID are both strings at runtime, but mixing them is a real bug. Branded types let you keep them distinct at the type level without runtime cost. You define a brand helper that intersects the underlying type with a unique tag, then use the branded alias for parameters and return types.

Now fetchUser takes a UserId, not just any string, and the compiler will catch the day someone passes an order ID by mistake. I use this for IDs, currency-typed numbers, sanitized HTML strings, and anything else where "the right kind of string" matters.

Type guards that earn their keep

I write small, named type guards for any boundary where untyped data enters my system: HTTP responses, queue messages, third-party SDK callbacks. The pattern is always the same: a function that accepts unknown and returns a typed predicate.

For anything more complex than a flat object I lean on a runtime validation library. Zod has become my default for new projects because the inferred types stay in lockstep with the runtime schema. One declaration, two guarantees.

Make impossible states impossible

A pattern from Elm that I keep coming back to. Instead of letting your types describe data that should never exist, design the type so it cannot.

A common example: a form that has an isLoading boolean and a data field. The type allows isLoading: true with data set, which is a state your code does not actually want to handle. Replace it with a discriminated union, and that state stops being expressible.

Each time I delete a defensive runtime check because the type system rules the case out, I treat that as a small win.

unknown over any, every time

any opts out of type checking. unknown says "I do not know what this is yet, force me to find out." For data crossing a trust boundary I always reach for unknown and validate before doing anything else. any belongs in two places: incremental migrations from JavaScript, and a single line escape hatch with a comment explaining why.

A linter rule banning any outside an allowlist is one of the highest-impact configuration changes I make on a new codebase.

as const for cheap refinement

as const is the smallest, most useful TypeScript feature most people forget exists. It freezes object literals into their narrowest type and gives you literal-typed enums without the overhead of enum. Pair a tuple of strings with as const and an indexed access type, and you have both a runtime list and a precise type, derived from a single source. I use this everywhere from feature flags to error codes.

satisfies for typed-but-narrow values

The satisfies operator is a quieter feature that pays huge dividends. It lets a value be checked against a type without widening it. The classic case is config objects: you want them validated against a known shape, but you also want the precise literal types preserved for downstream inference. With satisfies, a string field stays as its literal type rather than being widened to string, which makes it usable as a discriminator downstream.

A small note on type-level cleverness

TypeScript can encode startling things at the type level: SQL parsers, regex engines, parser combinators. Resist. In production codebases, the cost of clever types shows up in compile time, error messages, and the next engineer's morale. I use the powerful features sparingly and document them when I do. If a type takes more than a few minutes to read, I usually want to simplify it.

Project hygiene that compounds

Small habits I enforce on every TypeScript project I touch:

  • strict mode on, noUncheckedIndexedAccess on
  • A linter rule against any, with a curated allowlist
  • A pre-commit tsc --noEmit check
  • One source of truth for environment-variable types
  • Validators on every external boundary

These are not exciting. They are reliably the difference between a TypeScript codebase that becomes safer over time and one that quietly drifts back toward JavaScript with extra steps.

The mindset

Production TypeScript is mostly about modeling your domain precisely so the compiler can shoulder the boring checks. Used well, it lets a small team move fast on a complex codebase without the usual fear of breakage. Used poorly, it slows you down for no real benefit. The patterns above are the ones that consistently move it into the first category for me. If you want help shaping a TypeScript codebase along these lines, that is the kind of work I do through start project.

References

Tagged

#typescript#patterns#engineering
SV

Sri Vardhan

Independent technology studio of one. I help founders and small teams ship serious software without the consultancy overhead. More about me.

Want to discuss this topic?

I am always happy to dig deeper. If a piece sparked an idea or a disagreement, send it over. I read every message myself.

Get in Touch