Back to Blog
Engineering TypeScript

TypeScript Patterns We Use on Every Project

Ishara Perera · January 28, 2025

TypeScript Patterns We Use on Every Project

TypeScript is only as useful as the patterns you apply. Here are the ones we reach for on every project.

Prefer type over interface for data shapes

For plain data objects, type is simpler and more predictable. Reserve interface for when you need declaration merging or object-oriented patterns. The distinction matters less than consistency — pick one and stick to it.

Use satisfies to validate without widening

The satisfies operator lets you check that a value matches a type without losing the specificity of the inferred type. It’s particularly useful for config objects:

const config = {
  theme: 'dark',
  locale: 'en',
} satisfies Partial<AppConfig>;

Branded types for domain primitives

Distinguish between a raw string and a validated email address using branded types:

type Email = string & { __brand: 'Email' };

function toEmail(s: string): Email {
  if (!s.includes('@')) throw new Error('Invalid email');
  return s as Email;
}

This prevents accidentally passing an unvalidated string where a validated one is expected.

Exhaustive switch statements

Use never to ensure every case in a discriminated union is handled:

function assertNever(x: never): never {
  throw new Error('Unhandled case: ' + x);
}

The compiler will error if you add a new union member and forget to handle it.

Don’t fight the type system

The temptation to reach for any when types get complex is strong. Resist it. Complex types are usually a signal that the underlying design needs simplification, not that TypeScript needs to be bypassed.