Тема чистого кода — одна из самых обсуждаемых в сообществе разработчиков. Это не удивительно: от качества кода напрямую зависят скорость разработки, легкость поддержки и масштабирования проекта. В мире 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. Функциональный стиль предпочтительнее императивного
Проблема: Императивные циклы (for
, while
) для агрегации данных часто требуют ручного управления промежуточным состоянием (например, переменной-счетчиком 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
). Код стал короче, выразительнее и сфокусирован на что мы хотим сделать (посчитать сумму), а не на как это сделать (инициализировать счетчик, перебрать индексы, прибавить значение). Методы map
, filter
, reduce
— ваши лучшие друзья.
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)
monochromer
19.09.2025 08:45А здесь нужен interface, если будут классы-имплементаторы.
Делать реализацию от типа тоже можно
Rsa97
19.09.2025 08:45Теперь любой разработчик, увидев вызов функции, мгновенно поймет, что к дате добавляется определенное количество месяцев
Ну вит я увидел
addMonthToDate
и начал думать, а как к дате добавить месяц, например январь. Как минимумaddMonthsToDate
. А, возможно,Date.addInterval(interval: DateInterval)
, гдеinterface DateInterval { days?: number, months?: number, years?: number, };
sk_leks
Я бы добавил еще в дополнение к 7 пункту немаловажный аспект про слияние деклараций и возможность повтроного объявления с тем же именем у интерфейса, что часто используется к примеру для расширения базовых интерфейсов у сторонних библиотек или отдельных приложений.