5 TypeScript Tips I Wish I Knew Earlier
Practical TypeScript patterns and techniques that will level up your code quality and developer experience.
After years of writing TypeScript, here are the patterns I find myself using most often. These aren't the basics you'll find in every tutorial—they're the practical techniques that make a real difference.
1. Const Assertions for Literal Types
Instead of getting string[], get the exact literal types:
// Without const assertion
const colors = ['red', 'green', 'blue']; // string[]
// With const assertion
const colors = ['red', 'green', 'blue'] as const; // readonly ["red", "green", "blue"]
// Now you can derive a union type
type Color = typeof colors[number]; // "red" | "green" | "blue"
2. Discriminated Unions for State Management
Perfect for representing different states:
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function handleState(state: AsyncState<User>) {
switch (state.status) {
case 'idle':
return 'Click to load';
case 'loading':
return 'Loading...';
case 'success':
return `Hello, ${state.data.name}!`; // TypeScript knows data exists
case 'error':
return `Error: ${state.error.message}`; // TypeScript knows error exists
}
}
3. Template Literal Types
Create types from string patterns:
type EventName = `on${Capitalize<'click' | 'hover' | 'focus'>}`;
// "onClick" | "onHover" | "onFocus"
type CSSProperty = `${number}${'px' | 'rem' | 'em' | '%'}`;
// Matches "10px", "1.5rem", "100%", etc.
4. The satisfies Operator
Get type checking without losing literal types:
// With type annotation: loses literal types
const config: Record<string, string> = {
apiUrl: 'https://api.example.com',
env: 'production',
};
config.apiUrl; // string (not the literal)
// With satisfies: keeps literal types
const config = {
apiUrl: 'https://api.example.com',
env: 'production',
} satisfies Record<string, string>;
config.apiUrl; // "https://api.example.com"
5. Branded Types for Type Safety
Prevent mixing up similar types:
type UserId = string & { readonly brand: unique symbol };
type OrderId = string & { readonly brand: unique symbol };
function createUserId(id: string): UserId {
return id as UserId;
}
function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ }
const userId = createUserId('user_123');
getUser(userId); // ✅ Works
getOrder(userId); // ❌ Error: UserId is not assignable to OrderId
Conclusion
These patterns have significantly improved my TypeScript code. The key is understanding that TypeScript's type system is incredibly powerful—often the best solution involves leveraging the type system rather than working around it.
What TypeScript patterns do you find most useful? Let me know!