Back to Blog
Engineering
8 min read

TypeScript Patterns We Use in Every Production App

After shipping dozens of apps, these are the TypeScript patterns that actually matter — the ones that prevent bugs, improve readability, and scale cleanly.

M
Marcus Chen
April 26, 2026

TypeScript adds real value when used correctly. It adds noise and false confidence when used poorly. Here are the patterns we've settled on after shipping production apps for clients across industries.

Discriminated Unions for State Management

Instead of boolean flags that can conflict, use discriminated unions to model state explicitly.

```typescript type RequestState = | { status: 'idle' } | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: string }; ```

This makes impossible states impossible. You can't have both `data` and `error` defined. TypeScript will narrow correctly in every branch.

Branded Types for IDs

Raw string IDs are a footgun. Pass the wrong ID to the wrong function and TypeScript won't catch it.

```typescript type UserId = string & { readonly __brand: 'UserId' }; type OrderId = string & { readonly __brand: 'OrderId' }; ```

Now passing an `OrderId` where a `UserId` is expected is a compile error, not a runtime bug.

Zod for Runtime Validation at Boundaries

TypeScript types disappear at runtime. Any data crossing a system boundary — API responses, form inputs, environment variables — needs runtime validation.

We use Zod for this. Define the schema once, infer the TypeScript type from it, and validate at the boundary. Zero duplication.

```typescript const LeadSchema = z.object({ name: z.string().min(1), email: z.string().email(), service: z.enum(['web', 'seo', 'ai', 'mobile']), });

type Lead = z.infer; ```

Strict Null Checks and No Implicit Any

These should be enabled on every project from day one. Retrofitting a codebase that didn't have them is painful. Starting without them means TypeScript isn't actually protecting you.

Result Types Instead of Throwing

Functions that can fail should return a result type, not throw. Thrown errors are invisible in the type system.

```typescript type Result = | { ok: true; value: T } | { ok: false; error: E }; ```

Call sites now have to handle the error case. It's enforced by the compiler, not by code review.

The Pattern That Matters Most

Strict types at boundaries, flexible internals. Don't fight TypeScript in your core business logic — let it catch your mistakes. But don't let type gymnastics make simple code unreadable. Know when to use `as` and when to fix the types properly.

#TypeScript#Engineering#Best Practices#Web Development