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.