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.