← All writing
JavaScript

TypeScript Patterns Every Engineer Should Know

The best TypeScript is not the cleverest. It is the code where wrong states will not compile — using the type system as a design tool, not a chore you satisfy after the fact.

Plenty of teams use TypeScript and still ship the same class of bugs they did in JavaScript — because they treat types as paperwork to satisfy after the logic is written. Used well, the type system is a design tool: you model your domain so that incorrect states are not just discouraged but unrepresentable, and the compiler catches mistakes before they reach a test.

Loose modelling

Optional fields · Impossible combinations still compile · Bugs slip through

Discriminated union

One field per state · Only real states can exist · Compiler narrows for you

Make illegal states unrepresentable — model a value as one of its real shapes, not a bag of optional fields the compiler cannot police.

Make illegal states unrepresentable

The most valuable habit in TypeScript is shaping types so impossible combinations cannot be expressed. If a request is either loading, loaded with data, or failed with an error, do not model it as three optional fields you have to keep in sync — model it as one of three exact shapes.

// loose: every field optional, all combinations "valid"
type Bad = { loading?: boolean; data?: User; error?: string };

// tight: only the three real states can exist
type State =
  | { status: "loading" }
  | { status: "success"; data: User }
  | { status: "error"; error: string };

Discriminated unions are your workhorse

That pattern above is a discriminated union, and it is the single most useful tool in everyday TypeScript. A shared literal field — status here — lets the compiler narrow the type inside each branch, so accessing data is only allowed once you have checked status === "success". The result is exhaustive, self-documenting logic.

If you find yourself writing "this field exists, but only when that other field is set", you want a union.

Generics with restraint

Generics are powerful and easy to overdo. A good generic reads almost like plain English and earns its abstraction; a bad one turns a simple function into a puzzle of nested type parameters. Reach for a generic when a function genuinely works over many types in the same way — and stop the moment the signature becomes harder to read than the body.

Let inference do the work

You do not need to annotate everything. TypeScript's inference is strong — over-annotating adds noise and can actually hide better types the compiler would have derived. Annotate the boundaries (function parameters, public APIs, return types you want to lock) and let inference handle the interior.

  • Annotate inputs and public surfaces; infer the rest.
  • Prefer unions over optional-field soup for anything stateful.
  • Turn on strict — every flag it enables prevents a real bug.

Great TypeScript is quiet. It does not show off; it just makes the wrong thing fail to compile, so the bug you would have shipped becomes a red squiggle you fix in seconds.

Keep reading