After years of writing TypeScript, I've collected a handful of patterns that I reach for constantly. These aren't fancy tricks - they're practical approaches that make code easier to understand and maintain.
1. Discriminated Unions for State
Instead of boolean flags, use discriminated unions:
// Instead of this:
interface LoadingState {
isLoading: boolean;
isError: boolean;
data?: User;
error?: Error;
}
// Do this:
type UserState =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: User }
| { status: 'error'; error: Error };
The discriminated union makes impossible states impossible. You can't have both data and error simultaneously.
2. Const Assertions for Literals
When you need exact string values:
const ROUTES = {
home: '/',
projects: '/projects',
blog: '/blog',
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES];
// Type is '/' | '/projects' | '/blog', not string
3. Generic Constraints with extends
Make your generics more useful by constraining them:
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
// TypeScript knows the return type!
const user = { name: 'Annabelle', age: 30 };
const name = getProperty(user, 'name'); // type is string
4. Template Literal Types
Create precise string types:
type EventName = `on${Capitalize<'click' | 'focus' | 'blur'>}`;
// Type is 'onClick' | 'onFocus' | 'onBlur'
5. Satisfies for Type Checking Without Widening
New in TypeScript 4.9, satisfies is incredibly useful:
const colors = {
primary: '#d946ef',
secondary: '#06b6d4',
} satisfies Record<string, string>;
// colors.primary is typed as '#d946ef', not string!
Conclusion
These patterns share a common theme: they push more information into the type system, letting TypeScript catch bugs before they happen. The compiler becomes your pair programming partner, always checking your work.
What TypeScript patterns do you find most useful? I'd love to hear about them!