TypeScript Best Practices for 2024
Essential TypeScript best practices to write cleaner, more maintainable code with better type safety.
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!