mdashikjs/blog
All posts
5 TypeScript Patterns I Use in Every Production Codebase
Backend

5 TypeScript Patterns I Use in Every Production Codebase

Backend5 min

5 TypeScript Patterns I Use in Every Production Codebase

These are not clever tricks. They are patterns that prevent entire categories of bugs, make refactoring safe, and keep large codebases navigable. Every one is battle-tested.

TypeScriptPatternsBest Practices
Share:

1. Branded types for IDs

A userId and a postId are both strings, but passing one where the other is expected is always a bug. Branded types catch this at compile time:

type Brand<T, B> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type PostId = Brand<string, 'PostId'>;

function getUser(id: UserId) { /* ... */ }

const postId = 'abc' as PostId;
getUser(postId); // Type error

Zero runtime cost. The brand only exists in the type system.

2. Exhaustive switch with never

When you add a new variant to a union, every switch statement that handles it should break at compile time:

type Status = 'active' | 'paused' | 'cancelled';

function label(status: Status): string {
  switch (status) {
    case 'active': return 'Active';
    case 'paused': return 'Paused';
    case 'cancelled': return 'Cancelled';
    default: {
      const _exhaustive: never = status;
      return _exhaustive;
    }
  }
}

Add a new status like 'archived' and TypeScript will error on every switch that does not handle it.

3. Result types instead of thrown exceptions

Exceptions are invisible in the type system. A function that can fail should say so in its return type:

type Result<T, E = Error> =
  | { ok: true; data: T }
  | { ok: false; error: E };

async function parseConfig(raw: string): Result<Config, 'invalid_json' | 'missing_field'> {
  // ...
}

The caller is forced to handle both cases. No forgotten try-catch.

4. Const assertions for configuration objects

const ROLES = ['admin', 'editor', 'viewer'] as const;
type Role = typeof ROLES[number]; // 'admin' | 'editor' | 'viewer'

The union type stays in sync with the array automatically. Add a role to the array and the type updates. No duplication.

5. Zod schemas as the single source of truth

Define the shape once, derive both the runtime validator and the TypeScript type from it:

import { z } from 'zod';

const UserSchema = z.object({
  id: z.string().uuid(),
  email: z.string().email(),
  role: z.enum(['admin', 'editor', 'viewer']),
});

type User = z.infer<typeof UserSchema>;

The schema validates at runtime. The type checks at compile time. They can never drift apart.

MA

Written by Md Ashik

Senior Software Engineer building reliable backends. I write about the practical tradeoffs behind shipping software that holds up in production.