TypeScript’s type system is incredibly powerful. Here are some tips and patterns that will help you write better, safer code.
Utility Types
TypeScript provides several built-in utility types that are incredibly useful.
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Make all properties optional
type PartialUser = Partial<User>;
// Pick specific properties
type UserPreview = Pick<User, 'id' | 'name'>;
// Omit sensitive fields
type PublicUser = Omit<User, 'password'>;
// Make all properties readonly
type ReadonlyUser = Readonly<User>;
Template Literal Types
type Color = 'red' | 'blue' | 'green';
type Size = 'sm' | 'md' | 'lg';
type ClassName = `${Size}-${Color}`; // "sm-red" | "sm-blue" | ...
Discriminated Unions
Perfect for handling different states in your application.
type LoadingState = { status: 'loading' };
type ErrorState = { status: 'error'; message: string };
type SuccessState<T> = { status: 'success'; data: T };
type AsyncState<T> = LoadingState | ErrorState | SuccessState<T>;
function handleState(state: AsyncState<User[]>) {
switch (state.status) {
case 'loading':
return 'Loading...';
case 'error':
return `Error: ${state.message}`;
case 'success':
return state.data; // TypeScript knows data exists
}
}
The satisfies Operator
Ensures a value matches a type while preserving the narrowest possible type.
const config = {
apiUrl: 'https://api.example.com',
timeout: 5000,
retries: 3,
} satisfies Record<string, string | number>;
// config.apiUrl is still typed as string, not string | number
Key Takeaways
- Use utility types to avoid repetitive type definitions
- Discriminated unions make state handling type-safe
satisfiesgives you the best of both worlds- Use
constassertions for literal types - Generic constraints make functions more flexible yet type-safe