TypeScript Best Practices for 2024
TypeScript best practices: strict mode, interfaces, union types, utility types. Write type-safe, maintainable code. Free guide for developers.
TypeScript Best Practices for 2024
TypeScript has become the standard for large-scale JavaScript applications. Here are the essential best practices to write better TypeScript code.
1. Use Strict Mode
Always enable strict mode in your tsconfig.json:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
2. Prefer Interfaces Over Types
Use interfaces for object shapes:
interface User {
id: number;
name: string;
email: string;
isActive: boolean;
}
// Instead of:
type User = {
id: number;
name: string;
email: string;
isActive: boolean;
}
3. Use Union Types for State
Represent state with union types:
type LoadingState = 'idle' | 'loading' | 'success' | 'error';
interface ApiState<T> {
data: T | null;
state: LoadingState;
error: string | null;
}
4. Leverage Generic Constraints
Use generic constraints for better type safety:
interface HasId {
id: number;
}
function updateItem<T extends HasId>(item: T, updates: Partial<T>): T {
return { ...item, ...updates };
}
5. Use Discriminated Unions
For complex state management:
type ApiResponse<T> =
| { status: 'loading' }
| { status: 'success'; data: T }
| { status: 'error'; error: string };
function handleResponse<T>(response: ApiResponse<T>) {
switch (response.status) {
case 'loading':
return 'Loading...';
case 'success':
return response.data;
case 'error':
return response.error;
}
}
6. Prefer Readonly Arrays
Use readonly arrays when you don't need to mutate:
function processItems(items: readonly string[]): string[] {
return items.map(item => item.toUpperCase());
}
7. Use Utility Types
Leverage built-in utility types:
interface User {
id: number;
name: string;
email: string;
password: string;
}
// Create a type without password
type PublicUser = Omit<User, 'password'>;
// Make all properties optional
type PartialUser = Partial<User>;
// Pick specific properties
type UserName = Pick<User, 'name' | 'email'>;
8. Use Const Assertions
For literal types:
const colors = ['red', 'green', 'blue'] as const;
type Color = typeof colors[number]; // 'red' | 'green' | 'blue'
9. Prefer Function Declarations
Use function declarations for better hoisting:
function calculateTotal(items: Item[]): number {
return items.reduce((sum, item) => sum + item.price, 0);
}
10. Use Proper Error Handling
Create custom error types:
class ValidationError extends Error {
constructor(message: string, public field: string) {
super(message);
this.name = 'ValidationError';
}
}
function validateUser(user: unknown): User {
if (!user || typeof user !== 'object') {
throw new ValidationError('Invalid user data', 'user');
}
// ... validation logic
}
11. Use Template Literal Types
For string manipulation:
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE';
type ApiEndpoint = `/api/${string}`;
type ApiCall = {
method: HttpMethod;
endpoint: ApiEndpoint;
};
12. Prefer Composition Over Inheritance
Use composition patterns:
interface Logger {
log(message: string): void;
}
interface Database {
save(data: unknown): Promise<void>;
}
class UserService {
constructor(
private logger: Logger,
private database: Database
) {}
async createUser(user: User): Promise<void> {
this.logger.log('Creating user');
await this.database.save(user);
}
}
Conclusion
Following these best practices will help you write more maintainable, type-safe TypeScript code. Remember to:
- Use strict mode
- Prefer interfaces over types
- Leverage union types and discriminated unions
- Use utility types effectively
- Write proper error handling
- Choose composition over inheritance
These practices will make your code more robust and easier to maintain!