Why TypeScript Matters
TypeScript has evolved from a nice-to-have to an essential tool for modern web development. Here are the practices I've adopted in 2024.
1. Strict Mode is Non-Negotiable
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitReturns": true
}
}
2. Use satisfies for Type Validation
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
} satisfies Config;
// config.apiUrl is still string, not string | number
3. Discriminated Unions for State
type AsyncState<T> =
| { status: 'idle' }
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: Error };
function render(state: AsyncState<User>) {
switch (state.status) {
case 'idle':
return null;
case 'loading':
return <Spinner />;
case 'success':
return <UserCard user={state.data} />;
case 'error':
return <ErrorMessage error={state.error} />;
}
}
4. Branded Types for IDs
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); // OK
getOrder(userId); // Error! Type safety wins
5. Const Assertions
const ROUTES = {
HOME: '/',
ABOUT: '/about',
CONTACT: '/contact',
} as const;
type Route = typeof ROUTES[keyof typeof ROUTES];
// type Route = '/' | '/about' | '/contact'
6. Template Literal Types
type EventName = `on${Capitalize<string>}`;
function addEventListener(event: EventName, handler: () => void) {
// ...
}
addEventListener('onClick', () => {}); // OK
addEventListener('click', () => {}); // Error!
7. Utility Types
// Pick only what you need
type UserPreview = Pick<User, 'id' | 'name' | 'avatar'>;
// Make all properties optional
type PartialUser = Partial<User>;
// Make all properties required
type RequiredUser = Required<User>;
// Extract return type
type ApiResponse = Awaited<ReturnType<typeof fetchUser>>;
8. Generic Constraints
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: 'John', age: 30 };
getProperty(user, 'name'); // OK, returns string
getProperty(user, 'email'); // Error! 'email' not in User
Conclusion
TypeScript is a journey, not a destination. These practices have served me well, but always be open to new patterns as the language evolves.