5 TypeScript Patterns I Use Every Day

After years of writing TypeScript I keep returning to the same handful of patterns. Here are the five I reach for most.

1. Discriminated unions

Model state machines explicitly. A Result<T, E> type beats throwing exceptions for expected failures — callers are forced to handle the error case at the type level.

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

function divide(a: number, b: number): Result<number, string> {
  if (b === 0) return { ok: false, error: 'Division by zero' };
  return { ok: true, value: a / b };
}

const result = divide(10, 2);
if (result.ok) {
  console.log(result.value); // TypeScript knows this is number
}

2. satisfies operator

Validate a literal against a type while keeping the narrowed type. Without satisfies, you lose autocomplete on the individual values.

type Palette = Record<string, [number, number, number]>;

const colors = {
  red: [255, 0, 0],
  green: [0, 255, 0],
  blue: [0, 0, 255],
} satisfies Palette;

// Still works — TypeScript knows colors.red is a tuple, not just number[]
colors.red.map((channel) => channel / 255);

3. infer in conditional types

Extract type parameters from generic types at the call site.

type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;

type A = UnwrapPromise<Promise<string>>; // string
type B = UnwrapPromise<number>; // number

// Useful for pulling the resolved type out of async functions
type ResolvedReturn<F extends (...args: unknown[]) => unknown> = UnwrapPromise<ReturnType<F>>;

4. Template literal types

Generate string-union types from combinations — great for event names, CSS property maps, or API route builders.

type Side = 'top' | 'right' | 'bottom' | 'left';
type Axis = 'x' | 'y';

type PaddingProp = `padding-${Side}`;
// "padding-top" | "padding-right" | "padding-bottom" | "padding-left"

type TranslateKey = `translate${Uppercase<Axis>}`;
// "translateX" | "translateY"

5. NoInfer<T>

Prevent TypeScript from widening inference at specific positions. Classic example: a createStore where the initial value should pin the type, not let other arguments widen it.

function createStore<T>(initial: T, fallback: NoInfer<T>): T {
  return initial ?? fallback;
}

// Error: 'string' is not assignable to 'number'
createStore(42, 'hello');

// Fine
createStore(42, 0);

NoInfer shipped in TypeScript 5.4 and is one of the most quietly useful additions in recent years.

← All posts