5 TypeScript Patterns That Changed How I Code
Practical TypeScript patterns for writing more maintainable and type-safe code
January 5, 2025
5 TypeScript Patterns That Changed How I Code
After two years of TypeScript, these patterns have become essential to how I write code.
1. Discriminated Unions for State
Instead of optional properties everywhere:
// ❌ Hard to reason about
interface LoadingState {
isLoading: boolean;
data?: User[];
error?: string;
}
Use discriminated unions:
// ✅ Impossible states are impossible
type DataState =
| { status: 'loading' }
| { status: 'success'; data: User[] }
| { status: 'error'; error: string };
Now TypeScript knows when data exists:
if (state.status === 'success') {
// TypeScript knows state.data exists here!
state.data.forEach(user => ...)
}
2. Branded Types for IDs
Prevent mixing up different IDs:
type UserId = string & { readonly brand: unique symbol };
type PostId = string & { readonly brand: unique symbol };
function getUser(id: UserId) { ... }
function getPost(id: PostId) { ... }
const userId: UserId = 'user-123' as UserId;
const postId: PostId = 'post-456' as PostId;
getUser(postId); // ❌ TypeScript error!
No more accidentally passing the wrong ID type.
3. Const Assertions for Better Inference
// ❌ Type is string[]
const colors = ['red', 'blue', 'green'];
// ✅ Type is readonly ['red', 'blue', 'green']
const colors = ['red', 'blue', 'green'] as const;
// Now you can do:
type Color = typeof colors[number]; // 'red' | 'blue' | 'green'
4. Generic Constraints for Flexible APIs
function sortBy<T, K extends keyof T>(
items: T[],
key: K
): T[] {
return items.sort((a, b) =>
a[key] > b[key] ? 1 : -1
);
}
const users = [
{ name: 'Alice', age: 30 },
{ name: 'Bob', age: 25 }
];
sortBy(users, 'age'); // ✅ Works!
sortBy(users, 'email'); // ❌ TypeScript error - no 'email' property
5. Type Guards for Runtime Safety
function isUser(value: unknown): value is User {
return (
typeof value === 'object' &&
value !== null &&
'id' in value &&
'name' in value
);
}
// Now you can safely narrow types:
function processData(data: unknown) {
if (isUser(data)) {
// TypeScript knows data is User here
console.log(data.name);
}
}
Bonus: Utility Types
Don’t sleep on built-in utility types:
// Pick only certain properties
type UserPreview = Pick<User, 'id' | 'name'>;
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<User>;
// Exclude certain keys
type UserWithoutPassword = Omit<User, 'password'>;
Conclusion
TypeScript is more than just adding types to JavaScript. These patterns help you write code that’s:
- Safer (impossible states are impossible)
- More maintainable (types document intent)
- Easier to refactor (compiler catches breaks)
Start using one pattern this week. Your future self will thank you.