Clean Code на TypeScript: принципы и примеры для практиков

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

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                

Блоги

Рекомендаций по книгам