Тема чистого кода — одна из самых обсуждаемых в сообществе разработчиков. Это не удивительно: от качества кода напрямую зависят скорость разработки, легкость поддержки и масштабирования проекта. В мире 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 используете вы? Делитесь вашими любимыми практиками и примерами в комментариях!
