Clean Code на TypeScript: принципы и примеры на практике
Чистый код (Clean Code) — это не просто модный термин, а фундаментальный навык профессионального разработчика. В контексте TypeScript, который добавляет статическую типизацию в динамичный мир JavaScript, принципы чистого кода становятся ещё более важными. TypeScript даёт нам мощные инструменты для самодокументирования кода, но без правильного подхода они могут превратиться в хаос.
В этой статье мы разберём ключевые принципы Clean Code применительно к TypeScript и рассмотрим практические примеры, которые помогут вам писать более читаемый, поддерживаемый и надёжный код.
1. Именование: говорите на языке бизнеса
Первый и самый очевидный принцип Clean Code — правильное именование. Имена переменных, функций, классов и интерфейсов должны отражать их суть и намерение разработчика. TypeScript с его строгой типизацией позволяет сделать имена ещё более информативными.
Плохой пример:
// Что делает эта функция? Сколько времени нужно, чтобы это понять?
function d(a: number, b: number): number {
return a * b - (a + b);
}
// А что это за интерфейс?
interface I {
n: string;
a: number;
e: string;
}
Хороший пример:
// Функция расчёта скидки с учётом порога
function calculateDiscountedPrice(price: number, discountPercent: number): number {
const discountAmount = price * (discountPercent / 100);
return price - discountAmount;
}
// Интерфейс пользователя с явными полями
interface UserProfile {
name: string;
age: number;
email: string;
}
Правила для TypeScript:
- Интерфейсы — используйте существительные без префикса
I(например,User, а неIUser). Современный TypeScript и ESLint рекомендуют именно такой подход. - Типы — для сложных объединений используйте союз
|и осмысленные имена:StatusType,ApiResponse. - Булевы переменные — начинайте с
is,has,should:isActive,hasPermission. - Функции — используйте глаголы:
getUserData,validateEmail,processOrder.
2. SOLID в TypeScript: практическое применение
Принципы SOLID (Single Responsibility, Open-Closed, Liskov Substitution, Interface Segregation, Dependency Inversion) отлично ложатся на TypeScript благодаря поддержке интерфейсов, абстрактных классов и дженериков.
Принцип единственной ответственности (SRP): класс должен иметь только одну причину для изменения.
// Нарушение SRP: класс делает слишком много
class UserService {
createUser(data: any) { /* ... */ }
sendEmail(email: string) { /* ... */ }
generateReport() { /* ... */ }
}
// Правильный подход: разделение ответственности
class UserRepository {
save(user: User): void { /* ... */ }
}
class EmailService {
sendWelcomeEmail(email: string): void { /* ... */ }
}
class ReportGenerator {
generateUserReport(users: User[]): Report { /* ... */ }
}
Принцип подстановки Барбары Лисков (LSP): подклассы должны быть взаимозаменяемы с базовым классом. В TypeScript это особенно актуально при работе с наследованием.
// Нарушение LSP: подкласс сужает контракт
class Bird {
fly(): void { console.log('Flying'); }
}
class Penguin extends Bird {
fly(): void {
throw new Error('Penguins cannot fly!');
}
}
// Правильно: выделяем интерфейс
interface Flyable {
fly(): void;
}
class Bird implements Flyable {
fly(): void { console.log('Flying'); }
}
class Penguin {
swim(): void { console.log('Swimming'); }
}
Совет: Используйте interface и abstract class для строгого определения контрактов. TypeScript проверит их соблюдение на этапе компиляции.
3. Типизация: используйте силу TypeScript на полную
Одна из главных причин перехода на TypeScript — возможность избежать ошибок на этапе компиляции. Однако многие разработчики злоупотребляют типом any, сводя на нет все преимущества.
Плохой пример:
function processData(data: any): any {
return data.name + data.age;
}
// Компилятор не предупредит об ошибке
processData({ name: 'Alice' }); // age is undefined, но ошибка будет в рантайме
Хороший пример:
interface Person {
name: string;
age: number;
}
function processData(person: Person): string {
return `${person.name} is ${person.age} years old.`;
}
// Ошибка компиляции: missing age
// processData({ name: 'Alice' });
Продвинутые техники:
- Utility Types:
Partial<T>,Pick<T, K>,Omit<T, K>— используйте для гибкой работы с объектами. - Type Guards: сужайте типы с помощью пользовательских защитников.
- Discriminated Unions: для работы с разными вариантами данных.
// Discriminated Union — мощный паттерн
type Shape =
| { kind: 'circle'; radius: number }
| { kind: 'rectangle'; width: number; height: number };
function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'rectangle':
return shape.width * shape.height;
default:
// Exhaustive check: компилятор проверит, что все случаи обработаны
const _exhaustive: never = shape;
return _exhaustive;
}
}
4. Рефакторинг: как превратить "грязный" код в чистый
Рефакторинг — это не разовая акция, а постоянный процесс. Вот несколько конкретных приёмов для TypeScript.
Избавляемся от магических чисел:
// Плохо: что такое 86400000?
const time = 86400000;
// Хорошо: понятная константа
const MILLISECONDS_IN_A_DAY = 24 * 60 * 60 * 1000