Clean Code in TypeScript: Principles and Practical Examples
Clean Code is not just a trendy term, but a fundamental skill for a professional developer. In the context of TypeScript, which adds static typing to the dynamic world of JavaScript, clean code principles become even more important. TypeScript gives us powerful tools for self-documenting code, but without the right approach, they can turn into chaos.
In this article, we will break down the key Clean Code principles as applied to TypeScript and look at practical examples that will help you write more readable, maintainable, and reliable code.
1. Naming: Speak the Language of Business
The first and most obvious Clean Code principle is proper naming. The names of variables, functions, classes, and interfaces should reflect their essence and the developer's intent. TypeScript, with its strict typing, allows you to make names even more informative.
Bad example:
// What does this function do? How long does it take to understand?function d(a: number, b: number): number { return a * b - (a + b);}
// And what is this interface?interface I { n: string; a: number; e: string;}Good example:
// Function for calculating a discount considering a thresholdfunction calculateDiscountedPrice(price: number, discountPercent: number): number { const discountAmount = price * (discountPercent / 100); return price - discountAmount;}
// User interface with explicit fieldsinterface UserProfile { name: string; age: number; email: string;}Rules for TypeScript:
- Interfaces — use nouns without the
Iprefix (e.g.,User, notIUser). Modern TypeScript and ESLint recommend this approach. - Types — for complex unions, use the
|operator and meaningful names:StatusType,ApiResponse. - Boolean variables — start with
is,has,should:isActive,hasPermission. - Functions — use verbs:
getUserData,validateEmail,processOrder.
2. SOLID in TypeScript: Practical Application
The SOLID principles (Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) fit perfectly with TypeScript thanks to its support for interfaces, abstract classes, and generics.
Single Responsibility Principle (SRP): A class should have only one reason to change.
// SRP violation: the class does too muchclass UserService { createUser(data: any) { /* ... */ } sendEmail(email: string) { /* ... */ } generateReport() { /* ... */ }}
// Correct approach: separation of concernsclass UserRepository { save(user: User): void { /* ... */ }}
class EmailService { sendWelcomeEmail(email: string): void { /* ... */ }}
class ReportGenerator { generateUserReport(users: User[]): Report { /* ... */ }}Liskov Substitution Principle (LSP): Subclasses should be interchangeable with their base class. In TypeScript, this is especially relevant when working with inheritance.
// LSP violation: subclass narrows the contractclass Bird { fly(): void { console.log('Flying'); }}
class Penguin extends Bird { fly(): void { throw new Error('Penguins cannot fly!'); }}
// Correct: extract an interfaceinterface Flyable { fly(): void;}
class Bird implements Flyable { fly(): void { console.log('Flying'); }}
class Penguin { s