Тема чистого кода — одна из самых обсуждаемых в сообществе разработчиков. Это не удивительно: от качества кода напрямую зависят скорость разработки, легкость поддержки и масштабирования проекта. В мире JavaScript и TypeScript, с их гибкостью и динамической природой, следование принципам чистого кода становится особенно важным.

В этой статье мы разберем несколько ключевых принципов написания чистого, идиоматичного кода на TypeScript, которые помогут сделать ваши проекты более предсказуемыми, читаемыми и профессиональными. Погнали!

1. Избегайте избыточного контекста в именах

Проблема: Если имя типа или класса уже говорит само за себя, не нужно повторять эту информацию в именах его свойств. Это избыточно и создает лишний шум.

Плохо: Дублирование car в каждом свойстве.

type Car = {
  carMake: string;
  carModel: string;
  carColor: string;
}

function printCar(car: Car): void {
  console.log(`${car.carMake} ${car.carModel} (${car.carColor})`);
}

Хорошо: Имя типа Car уже задает контекст.

type Car = {
  make: string;
  model: string;
  color: string;
}

function printCar(car: Car): void {
  console.log(`${car.make} ${car.model} (${car.color})`);
}

Что изменилось: Код стал короче, читабельнее, а его смысл не изменился. Мы убрали ненужный префикс car, так как он уже содержится в имени типа.

2. Используйте enum

Проблема: Часто нам важна не конкретная строка или число, а сам факт выбора из предопределенного набора значений. Использование простых объектов для этого — не лучшая практика, так как они не предоставляют строгой типизации.

Плохо: Использование объекта как псевдо-enum.

const GENRE = {
  ROMANTIC: 'romantic',
  DRAMA: 'drama',
  COMEDY: 'comedy',
  DOCUMENTARY: 'documentary',
}

projector.configureFilm(GENRE.COMEDY);
// В методе configureFilm придется делать проверки на строки

Хорошо: Использование enum.

enum Genre {
  Romantic,
  Drama,
  Comedy,
  Documentary,
}

projector.configureFilm(Genre.Comedy);

class Projector {
  configureFilm(genre: Genre) {
    switch (genre) {
      case Genre.Romantic:
        // Логика для романтического фильма
        break;
    }
  }
}

Что изменилось: enum в TypeScript создает новый тип и гарантирует, что в configureFilm будет передано только одно из допустимых значений. Код становится самодокументируемым и менее подвержен ошибкам, связанным с опечатками в строках.

Примечание: Для строковых enum можно использовать синтаксис enum Genre { Romantic = 'romantic' }, если нужно сериализовать значение в строку.

3. Имена функций должны отражать их суть

Проблема: Расплывчатые имена функций заставляют читателя гадать, что же именно они делают. Это замедляет понимание кода и может привести к ошибкам.

Плохо: Что такое «add»? Дни? Месяцы? Годы?

function addToDate(date: Date, month: number): Date {
  // ...
}

const date = new Date();
addToDate(date, 1); // Неочевидно, что добавляется

Хорошо: Имя функции четко описывает производимое действие.

function addMonthToDate(date: Date, month: number): Date {
  // ...
}

const date = new Date();
addMonthToDate(date, 1);

Что изменилось: Теперь любой разработчик, увидев вызов функции, мгновенно поймет, что к дате добавляется определенное количество месяцев. Не экономьте на словах в именах функций!

4. Функциональный стиль предпочтительнее императивного

Проблема: Императивные циклы (forwhile) для агрегации данных часто требуют ручного управления промежуточным состоянием (например, переменной-счетчиком totalOutput), что усложняет код и повышает вероятность ошибок.

Плохо: Императивный подход с циклом и изменяемой переменной.

const contributions = [/* ... массив объектов с полем linesOfCode ... */];
let totalOutput = 0;

for (let i = 0; i < contributions.length; i++) {
  totalOutput += contributions[i].linesOfCode;
}

Хорошо: Декларативный подход с методом reduce.

const contributions = [/* ... массив объектов с полем linesOfCode ... */];

const totalOutput = contributions
  .reduce((totalLines, contribution) => totalLines + contribution.linesOfCode, 0);

Что изменилось: Мы избавились от изменяемого состояния (let totalOutput). Код стал короче, выразительнее и сфокусирован на что мы хотим сделать (посчитать сумму), а не на как это сделать (инициализировать счетчик, перебрать индексы, прибавить значение). Методы mapfilterreduce — ваши лучшие друзья.

5. Избегайте негативных проверок в именах функций

Проблема: Когда имя функции содержит отрицание (например, Not), это заставляет наш мозг выполнять лишнюю логическую операцию при чтении условия. Условие if (isEmailNotUsed(email)) требует мысленно преобразовать «если email НЕ используется» в «если email свободен». Прямое утверждение (isEmailUsed) читается и понимается легче.

Плохо: Отрицание в имени функции. Логика условия if (isEmailNotUsed(email)) становится менее интуитивной.

function isEmailNotUsed(email: string): boolean {
  // ...
}

if (isEmailNotUsed(email)) {
  // ...
}

Хорошо: Позитивное утверждение в имени функции. Условие с явным отрицанием if (!isEmailUsed(email)) проще для восприятия.

function isEmailUsed(email: string): boolean {
  // ...
}

if (!isEmailUsed(email)) {
  // ...
}

Что изменилось: Условие if (!isEmailUsed(email)) читается проще, чем исходный вариант. Мы проверяем, что email «не использован», что интуитивно понятнее, чем «если не-не-использован». Старайтесь, чтобы булевы функции возвращали true для ожидаемого, позитивного условия.

6. Иммутабельность

Проблема: Изменяемые структуры данных могут быть неожиданно изменены в разных частях программы, что приводит к трудноотлавливаемым багам.

Плохо: Все свойства конфигурации можно изменить в runtime.

interface Config {
  host: string;
  port: string;
  db: string;
}

const config: Config = {...};
config.db = 'new_db'; // Потенциально нежелательное изменение

Хорошо: Использование readonly для защиты от изменений.

interface Config {
  readonly host: string;
  readonly port: string;
  readonly db: string;
}

const config: Config = {...};
config.db = 'new_db'; // Ошибка компиляции: Cannot assign to 'db' because it is a read-only property.

Что изменилось: Компилятор TypeScript теперь не позволит изменить свойства Config после их первоначального задания. Это делает код предсказуемее и защищает от случайных мутаций. Для объектов и массивов можно также использовать утилиты типов Readonly<T> и ReadonlyArray<T>.

7. type vs interface — осознанный выбор

Проблема: В TypeScript и type, и interface можно использовать для описания форм объектов, но у них есть важные различия.

Не строго плохо, но часто не оптимально:

interface EmailConfig { ... }
interface DbConfig { ... }
interface Config { ... } // Композиция через extends?

type Shape = { ... } // А здесь нужен interface, если будут классы-имплементаторы.

Хорошо: Используйте правильный инструмент для задачи.

// type для композиции (объединения или пересечения типов)
type EmailConfig = { ... }
type DbConfig = { ... }
type Config = EmailConfig | DbConfig; // Config - это либо один, либо другой

// interface для ООП-иерархий (extends/implements)
interface Shape {
  area(): number;
}

class Circle implements Shape {
  area() { ... }
}

class Square implements Shape {
  area() { ... }
}

Что изменилось: Есть простое правило:

  • Используйте type, когда вам могут понадобиться объединения (|), пересечения (&) или вы описываете тип-примитив.

  • Используйте interface, если вы хотите использовать классическое ООП с extends или implements. Интерфейсы лучше работают с ошибками в IDE.

Строгого правила нет, но главное — быть последовательным в рамках проекта.

8. Один концепт — один тест

Проблема: Большие тесты, проверяющие множество сценариев сразу, трудно читать и отлаживать. Если такой тест падает, не сразу понятно, какое именно условие не выполнилось.

Плохо: Три разных сценария в одном тесте.

it('handles date boundaries', () => { // Тестирует ВСЕ?
  let date: AwesomeDate;

  date = new AwesomeDate('1/1/2015');
  assert.equal('1/31/2015', date.addDays(30));

  date = new AwesomeDate('2/1/2016');
  assert.equal('2/29/2016', date.addDays(28));

  date = new AwesomeDate('2/1/2015');
  assert.equal('3/1/2015', date.addDays(28));
});

Хорошо: Разделение на три независимых теста.

it('should handle 30-day months', () => {
  const date = new AwesomeDate('1/1/2015');
  assert.equal('1/31/2015', date.addDays(30));
});

it('should handle leap year', () => {
  const date = new AwesomeDate('2/1/2016');
  assert.equal('2/29/2016', date.addDays(28));
});

it('should handle non-leap year', () => {
  const date = new AwesomeDate('2/1/2015');
  assert.equal('3/1/2015', date.addDays(28));
});

Что изменилось: Каждый тест проверяет ровно одну вещь и имеет четкое, понятное имя. Если один из сценариев упадет, мы сразу увидим, какой именно, по имени упавшего теста. Это следует принципу единственной ответственности (SRP), примененному к модульным тестам.

Заключение

Чистый код — это не догма, а набор практик, которые делают жизнь разработчика и всей команды проще. TypeScript с его мощной системой типов предоставляет отличные возможности для написания такого кода.

Конечно, в одной статье не уместить все аспекты чистого кода. Это огромная и интересная тема, включающая принципы SOLID, паттерны проектирования и многое другое.

А какие принципы чистого кода в TypeScript используете вы? Делитесь вашими любимыми практиками и примерами в комментариях!

Комментарии (0)


  1. sk_leks
    19.09.2025 08:45

    Я бы добавил еще в дополнение к 7 пункту немаловажный аспект про слияние деклараций и возможность повтроного объявления с тем же именем у интерфейса, что часто используется к примеру для расширения базовых интерфейсов у сторонних библиотек или отдельных приложений.

    interface User {
      id: number;
    }
    
    interface User {
      name: string;
    }
    
    // В результате слияния двух интерфейсов: { id: number; name: string }
    const user: User = { id: 1, name: "Nikita" };


  1. monochromer
    19.09.2025 08:45

    А здесь нужен interface, если будут классы-имплементаторы.

    Делать реализацию от типа тоже можно


  1. Rsa97
    19.09.2025 08:45

    Теперь любой разработчик, увидев вызов функции, мгновенно поймет, что к дате добавляется определенное количество месяцев

    Ну вит я увидел addMonthToDate и начал думать, а как к дате добавить месяц, например январь. Как минимум addMonthsToDate. А, возможно, Date.addInterval(interval: DateInterval), где

    interface DateInterval {
      days?: number,
      months?: number,
      years?: number,
    };