Как писать качественный код на JavaScript и TypeScript

Курс про то, как проектировать, писать и сопровождать читаемый, надежный и масштабируемый код на JS и TS. Разберём архитектурные принципы, типизацию, тестирование, рефакторинг, производительность и практики командной разработки.

1. Читаемость и стиль: именование, структура, чистые функции, код-ревью

Читаемость и стиль: именование, структура, чистые функции, код-ревью

Читаемость — базовая валюта качественного кода. Большую часть времени код читают (вы, коллеги, вы через полгода), а пишут — сравнительно мало. Эта статья задаёт фундамент курса: как делать код понятным на уровне имён, структуры, функций и командных практик.

!Пирамида приоритетов: сначала читаемость и стиль, потом остальные улучшения

Что такое читаемость в JS и TS

Читаемость — это когда по коду понятно:

  • Что делает программа на уровне бизнес-смысла.
  • Почему она делает это именно так (видно из структуры, названий и явных решений).
  • Где искать нужное место для изменения.
  • Какие ограничения у данных (особенно важно в TypeScript).
  • В JavaScript и TypeScript читаемость особенно важна из-за:

  • динамической природы JS и риска скрытых ошибок;
  • большого количества асинхронного кода;
  • долгоживущих фронтенд/бэкенд проектов, где код постоянно меняется.
  • Именование: главный источник смысла

    Хорошие имена уменьшают необходимость в комментариях и снижают когнитивную нагрузку.

    Принципы хороших имён

  • Имя отвечает на вопрос что это такое и зачем оно нужно.
  • Избегайте сокращений, понятных только автору: usr, tmp, data1.
  • Используйте язык предметной области: invoice, subscription, shipment вместо абстрактных item, entity.
  • Имя должно быть достаточно длинным, чтобы быть точным, но не шумным.
  • Правила по категориям

  • Переменные: существительные, отражающие смысл: user, cartTotal.
  • Функции: глагол + объект: calculateTotal, formatPrice, fetchUserById.
  • Булевы значения: вопрос/состояние: isLoading, hasAccess, shouldRetry.
  • Коллекции: множественное число: users, orders.
  • Обработчики событий: handleClick, onSubmit (выберите стиль и придерживайтесь).
  • Пример: плохое и хорошее именование

    TypeScript помогает сделать намерение ещё явнее: типы в примере не просто «для компилятора», они документируют контракт функции.

    Тонкие моменты

  • Не кодируйте тип в имени: userObj, usersArray — лишний шум. В TS тип и так есть.
  • Не используйте отрицательные булевы там, где можно положительные: isEnabled читабельнее, чем isNotDisabled.
  • Уточняйте единицы измерения: timeoutMs, sizeBytes, priceCents.
  • Структура кода: чтобы было где искать

    Именование отвечает за смысл на микроуровне, структура — на макроуровне.

    Структура файлов и модулей

    Практичные правила:

  • Один файл — одна основная ответственность.
  • Группируйте код по сценариям использования, а не по «типам сущностей», если это упрощает навигацию.
  • Разделяйте публичный и внутренний API модуля.
  • Пример простого подхода:

  • src/features/payments/ — всё, что относится к платежам (UI, сервисы, типы).
  • src/shared/ — переиспользуемые утилиты, UI-элементы, типы.
  • Локальная структура внутри файла

    Читатель обычно скроллит сверху вниз. Помогайте ему:

  • Сначала экспортируемые функции/классы, ниже — вспомогательные.
  • Держите рядом код, который меняется вместе.
  • Минимизируйте «прыжки глазами» по файлу.
  • Контроль сложности: меньше вложенности

    Главный приём — guard clauses (ранние выходы) вместо глубоких if.

    Такой код легче читать: «сначала исключения, потом основной сценарий».

    Меньше «магии»

    Избегайте:

  • скрытых глобальных зависимостей;
  • неявных преобразований типов;
  • «умных» трюков ради краткости.
  • Краткость хороша, когда она не ухудшает понимание.

    Чистые функции: предсказуемость как стиль

    Чистая функция — это функция, которая:

  • при одинаковых входных данных всегда возвращает одинаковый результат;
  • не имеет побочных эффектов (не меняет внешнее состояние, не пишет в лог, не делает запросы, не мутирует аргументы).
  • Чистые функции проще тестировать, рефакторить и переиспользовать.

    Пример: функция с побочным эффектом и чистая версия

    Ключевая идея: либо вы явно меняете состояние (и тогда это отдельный слой), либо вы вычисляете результат.

    Разделяйте «вычисления» и «эффекты»

    Один из самых полезных паттернов в реальных проектах:

  • Чистая часть подготавливает данные.
  • Отдельная часть выполняет I/O: сеть, база данных, DOM, файловая система.
  • Так код становится модульным: расчёт можно тестировать без сети.

    Практические правила для функций

  • Одна функция — одна понятная задача.
  • Чем меньше параметров, тем лучше (если параметров много, подумайте о параметр-объекте).
  • Избегайте булевых параметров, которые меняют поведение «внутри»: format(price, true) хуже, чем formatPriceWithCurrency(price).
  • Возвращаемый тип должен быть очевиден из имени: get... возвращает значение, create... создаёт и возвращает, update... меняет.
  • Стиль и автоматизация: договоритесь один раз

    Люди не должны спорить о пробелах и кавычках на код-ревью. Это решают инструменты.

    Минимальный набор в JS/TS проекте

  • Prettier для форматирования кода: единый стиль без обсуждений.
  • ESLint для правил качества: ошибки, анти-паттерны, опасные места.
  • Проверки в CI: стиль должен соблюдаться автоматически.
  • Официальные сайты:

  • Prettier
  • ESLint
  • Единый стиль: что именно стандартизировать

  • кавычки (одинарные/двойные);
  • точки с запятой (использовать или нет);
  • длину строки;
  • порядок импортов;
  • правила именования (особенно для TS типов и интерфейсов).
  • Важно: стиль должен быть последовательным. «Лучший» стиль — тот, который принят и автоматически поддерживается.

    Style guide как опора

    Можно ориентироваться на готовые руководства:

  • Airbnb JavaScript Style Guide
  • Google JavaScript Style Guide
  • Не обязательно копировать всё целиком. Важнее — зафиксировать правила и автоматизировать.

    Код-ревью: процесс, который делает код сильнее

    Код-ревью — это не экзамен и не «поиск виноватого». Это совместное улучшение качества и обмен знаниями.

    Цели код-ревью

  • Поймать дефекты до продакшена.
  • Улучшить читаемость и поддерживаемость.
  • Снизить риск сложных решений, которые трудно сопровождать.
  • Распространить знание о кодовой базе.
  • Что проверять в первую очередь

    Практический порядок проверки:

  • Корректность: правильна ли логика, покрыты ли крайние случаи.
  • Понятность: читается ли код без объяснений автора.
  • Архитектурные последствия: не создаём ли мы долг в виде плохой зависимости.
  • Тестируемость: можно ли это безопасно менять позже.
  • Стиль: только если это не закрыто автоматикой.
  • Чек-лист ревьюера

  • Имена отражают предметную область?
  • Функции небольшие и делают одно дело?
  • Нет скрытых побочных эффектов?
  • Ошибки обрабатываются явно?
  • Нет дублирования?
  • Типы TS помогают пониманию, а не усложняют?
  • Как писать комментарии на ревью

    Хороший комментарий:

  • конкретный;
  • объясняет почему;
  • предлагает вариант.
  • Пример формулировок:

  • Почему: «Здесь сложнее читать из-за вложенности. Можно сделать ранний return?»
  • Предложение: «Можно вынести расчёт в calculateReceipt, а отправку оставить в submitReceipt — будет проще тестировать».
  • Размер PR и привычка к маленьким изменениям

    Чем меньше PR, тем выше качество ревью. Практика:

  • ограничивайте PR одним смысловым изменением;
  • не смешивайте рефакторинг и фичу без необходимости;
  • фиксируйте договорённости в описании PR.
  • Для структуры сообщений коммитов полезен общий стандарт:

  • Conventional Commits
  • Как это связано с остальным курсом

    Эта статья — фундамент. Дальше (в следующих темах курса) мы будем усиливать качество за счёт:

  • типов и моделирования данных в TypeScript;
  • обработки ошибок и контрактов;
  • тестирования и подходов к рефакторингу;
  • работы с асинхронностью и предсказуемостью.
  • Если сейчас вы начнёте стабильно применять правила именования, структурирования и разделения чистых вычислений от эффектов — почти все последующие практики будут даваться заметно легче.

    Полезные официальные справочники

  • TypeScript Handbook
  • MDN JavaScript Guide
  • 2. Надёжный JavaScript: ошибки, асинхронность, промисы, исключения, edge cases

    Надёжный JavaScript: ошибки, асинхронность, промисы, исключения, edge cases

    Надёжность в JavaScript и TypeScript — это способность кода предсказуемо работать в реальных условиях: при нестабильной сети, неожиданных данных, конкурирующих запросах, разных окружениях и «редких» состояниях.

    В предыдущей теме мы строили фундамент читаемости: хорошие имена, структура, чистые функции, код-ревью. Надёжность логично продолжает эту линию: мы делаем так, чтобы ошибки не превращались в хаос, а асинхронность не создавала скрытых дефектов.

    !Поясняет, где удобно держать чистую логику, а где обрабатывать и логировать ошибки

    Что считать ошибкой

    В коде встречаются разные «плохие» ситуации, и от типа ситуации зависит правильная стратегия.

  • Ожидаемые ошибки — нормальная часть бизнес-процесса: «неверный пароль», «товара нет в наличии», «лимит исчерпан».
  • Неожиданные ошибки — баги, нарушения инвариантов, поломки инфраструктуры: «undefined там, где не должно быть», «500 от сервера», «не распарсился формат, который всегда был валидным».
  • Практическое правило:

  • Ожидаемые ошибки удобнее представлять как значения (например, Result-объект), чтобы вызывающий код обязан был обработать сценарий.
  • Неожиданные ошибки допустимо выражать через исключения (throw) и rejection промисов, но с дисциплиной: не глотать, не терять контекст, обрабатывать на границах.
  • Исключения в синхронном коде: throw, try/catch/finally

    Базовая механика

  • throw прерывает выполнение текущей функции и «поднимает» ошибку вверх по стеку вызовов.
  • try/catch перехватывает исключение только в том потоке выполнения, где оно произошло.
  • finally выполнится всегда: и при успехе, и при ошибке.
  • Старайтесь, чтобы try был маленьким: чем меньше кода внутри, тем легче понять, что именно может выбросить ошибку.

    Официальная справка: MDN: try...catch

    Пользовательские ошибки и контекст

    Когда ошибка важна для диагностики, полезно иметь специализированный класс ошибки.

  • В имени ошибки отражайте доменный смысл: ValidationError, AuthError, ExternalServiceError.
  • Храните полезные поля: statusCode, requestId, details.
  • Если окружение поддерживает Error с причиной, можно сохранять исходную ошибку как контекст.

    Официальная справка: MDN: Error

    Никогда не «глотайте» ошибки молча

    Плохой паттерн — поймать и ничего не сделать.

    Если вы не можете обработать ошибку правильно, обычно нужно:

  • либо пробросить её дальше (throw err),
  • либо обернуть более понятной ошибкой и пробросить,
  • либо преобразовать в ожидаемый результат (Result), если вы на границе доменной логики.
  • Асинхронность: промисы, async/await и распространённые ловушки

    Как «путешествуют» ошибки в промисах

    У промиса есть два результата:

  • fulfilled (успех) со значением,
  • rejected (ошибка) с причиной.
  • Важно: throw внутри async-функции превращается в rejected promise.

    Официальная справка: MDN: Promise

    try/catch и await

    try/catch перехватит асинхронную ошибку только если вы await промис внутри try.

    Официальная справка: MDN: async function

    Самая частая ошибка: забыли await

    Если вы забыли await, try/catch не поймает rejection, потому что ошибка произойдёт позже, уже вне блока try.

    Проблема появляется, когда promise создаётся, но его rejection никто не ждёт и не обрабатывает.

    Практика:

  • всегда await асинхронные операции, от которых зависит корректность,
  • или явно возвращайте промис вызывающему коду,
  • или в явном виде навешивайте .catch(...) там, где это действительно уместно.
  • Цепочки промисов и «пропавший return»

    В .then(...) важно возвращать промис или значение, если вы строите цепочку.

    Если забыть return внутри then, наружу уйдёт undefined, и логика сломается без очевидных симптомов.

    Ошибка как значение: Result для ожидаемых сценариев

    Для ожидаемых ошибок (валидация, отказ доступа, отсутствие данных) часто удобнее вернуть тип, который заставляет обработать оба исхода.

    TypeScript-справка по сужению типов: TypeScript Handbook: Narrowing

    Комбинаторы промисов: all, allSettled, race, any

    Эти методы задают семантику конкуренции и агрегации. Выбор неправильного комбинатора часто порождает edge cases.

    | Инструмент | Когда подходит | Ключевое поведение | | --- | --- | --- | | Promise.all | Нужны все результаты, ошибка любого рушит операцию | Fail-fast: первый rejection отклоняет общий промис | | Promise.allSettled | Нужно дождаться всех и собрать статусы | Всегда fulfilled со списком статусов | | Promise.race | Нужен результат первого завершившегося (успех или ошибка) | Завершается первым settled | | Promise.any | Нужен первый успешный результат из нескольких источников | Rejected только если все rejected |

    Официальная справка: MDN: Promise.all

    Таймауты, ретраи, отмена: делаем I/O управляемым

    В реальности «висеть вечно» нельзя, а повторять запросы нужно осознанно.

    Таймаут

    В браузере и Node.js можно реализовать таймаут через AbortController (если API поддерживает сигнал отмены, например fetch).

    Официальная справка: MDN: AbortController

    Ретрай

    Ретрай уместен не всегда.

  • Повторять можно операции, которые безопасны при повторе (обычно GET, иногда идемпотентные PUT).
  • Ретрай опасен для неидемпотентных операций (например, создание заказа), если нет идемпотентного ключа.
  • Чтобы избежать «шторма», часто используют задержку между попытками и ограничение числа попыток.

    Здесь задержка фиксированная для простоты; в продакшене часто применяют экспоненциальную задержку.

    Отмена операций и «зомби-обновления»

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

    Практики:

  • отменять запросы при смене контекста,
  • проверять актуальность перед применением результата,
  • хранить requestId и применять результат только для последнего запроса.
  • Обработка ошибок на границах приложения

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

    Разумное разделение:

  • Внутри доменной логики: возвращаем Result для ожидаемых сценариев, либо кидаем исключения на нарушения инвариантов.
  • На границах (HTTP-эндпоинт, UI-обработчик, CLI): превращаем ошибки в понятный ответ, логируем, добавляем корреляционные идентификаторы.
  • Пример для бэкенда (упрощённо):

    Ключевой принцип: не смешивать бизнес-логику с инфраструктурной обработкой.

    Edge cases в JavaScript: что ломает надёжность чаще всего

    Edge case — это корректный, но редко встречающийся вход или состояние, на котором код ведёт себя неожиданно.

    null и undefined

  • null обычно означает явное отсутствие значения.
  • undefined часто означает «не задано» или «не нашли».
  • Практики:

  • в TypeScript включайте strict режим,
  • используйте явные проверки и полезные операторы языка.
  • Оператор ?? не подменяет "" и 0, в отличие от ||, и поэтому обычно лучше для значений по умолчанию.

    Официальная справка: MDN: Nullish coalescing (??)

    NaN и проверки чисел

  • NaN «заразный»: операции с ним дают NaN.
  • NaN не равен самому себе (NaN !== NaN).
  • Используйте Number.isNaN(...) вместо глобального isNaN(...), чтобы избежать неявных преобразований.

    Официальная справка: MDN: Number.isNaN

    Парсинг чисел: parseInt и основание системы счисления

    Всегда указывайте основание:

    Это делает намерение явным и защищает от странных входных данных.

    Официальная справка: MDN: parseInt

    Дробные числа и деньги

    number в JS — это числа с плавающей точкой, и они не подходят для точных денежных расчётов.

    Практики:

  • хранить деньги в целых единицах (например, в центах) как number или bigint,
  • форматировать для отображения на последнем шаге.
  • Даты, часовые пояса и сериализация

    Даты — постоянный источник ошибок.

    Практики:

  • в API фиксируйте формат (часто ISO 8601),
  • храните и передавайте время в UTC,
  • аккуратно относитесь к локали и часовому поясу на UI.
  • Официальная справка: MDN: Date

    JSON: не всё сериализуется как вы ожидаете

    Edge cases JSON:

  • undefined в объектах исчезает при JSON.stringify,
  • NaN и Infinity превращаются в null,
  • bigint не сериализуется стандартным JSON.
  • Официальная справка: MDN: JSON.stringify

    Unhandled rejections: ошибки, которые «выстреливают» позже

    Если промис отклонён, но у него нет обработчика .catch(...) и его никто не await-ит, вы получаете unhandled rejection.

    В браузере можно слушать глобальное событие:

  • unhandledrejection — когда rejection не обработан.
  • Официальная справка: MDN: unhandledrejection event

    Практика:

  • в приложении должны быть понятные «границы», где ошибки логируются,
  • в тестах и CI unhandled rejections должны падать сборку,
  • избегайте «забытых» промисов (часто это побочный эффект неправильной структуры кода).
  • Мини-чеклист надёжности для JS/TS

  • Ошибки разделены на ожидаемые (значения) и неожиданные (исключения/rejection).
  • try/catch стоит на границе и оборачивает только то, что нужно.
  • В async коде нет «потерянных» промисов: всё либо await, либо возвращается наружу, либо имеет осмысленный .catch.
  • Выбран правильный комбинатор промисов (all vs allSettled и т.д.).
  • В I/O есть таймаут и, при необходимости, отмена.
  • В edge cases есть явные проверки: null/undefined, числа, JSON, даты.
  • Существуют глобальные обработчики и логирование для диагностики.
  • Эти практики усиливают то, что мы заложили ранее: читаемый код проще покрыть защитами, а разделение «чистые вычисления vs эффекты» делает обработку ошибок ясной и управляемой.

    3. TypeScript глубоко: типы, дженерики, narrowing, API-контракты, типобезопасный дизайн

    TypeScript глубоко: типы, дженерики, narrowing, API-контракты, типобезопасный дизайн

    TypeScript усиливает качество кода не тем, что запрещает писать, а тем, что помогает прояснять намерения и фиксировать контракты. Он превращает «неявные договорённости в голове» в проверяемые правила.

    Связь с предыдущими темами курса:

  • Из статьи про читаемость и стиль берём идею, что код должен быть понятен по именам и структуре. Типы в TS — продолжение этой мысли: они документируют смысл данных и поведения.
  • Из статьи про надёжный JavaScript берём дисциплину обработки ошибок и границы приложения. TS помогает сделать ожидаемые сценарии (включая ошибки) явными в сигнатурах, а неожиданные — локализовать.
  • !Диаграмма, показывающая место TypeScript в архитектуре: типы не заменяют рантайм-проверки, а делают доменную часть предсказуемой

    TypeScript как инструмент качества

    TypeScript даёт три практические выгоды для качества:

  • Предсказуемые изменения: компилятор подсвечивает места, которые нужно обновить при рефакторинге.
  • Самодокументируемые контракты: типы объясняют, что принимает и возвращает функция.
  • Снижение числа edge cases: благодаря strict-проверкам, narrowing и моделированию состояний.
  • При этом важно помнить:

  • TypeScript не гарантирует корректность данных в рантайме.
  • Типы должны помогать читать код. Если типовая система становится «головоломкой», это ухудшает качество.
  • Официальная база: TypeScript Handbook

    Режим strict как обязательный минимум

    Для реальных проектов strict — это не «хардкор», а нормальная защита от самых дорогих ошибок.

    Что даёт strict на практике:

  • null и undefined перестают «протекать» незаметно.
  • Нельзя случайно использовать значение типа any без явного решения.
  • Компилятор лучше сужает типы и предупреждает о недостижимых ветках.
  • Рекомендация:

  • включайте "strict": true в tsconfig.json для приложений и библиотек;
  • точечно ослабляйте правила только там, где это оправдано границей системы.
  • Справка: tsconfig strict

    any vs unknown: безопасная дисциплина на границах

    any выключает проверку типов и разрушает смысл TypeScript как инструмента качества.

    unknown — безопасная альтернатива для данных из внешнего мира.

  • unknown нельзя использовать без проверки.
  • any можно делать чем угодно, и компилятор «согласится».
  • Практическое правило:

  • на границах (HTTP, localStorage, env, внешние SDK) используйте unknown;
  • внутри доменной логики держите строгие типы.
  • Narrowing: как TypeScript делает код точнее

    Narrowing — это процесс, когда TS сужает тип значения на основе проверок в коде.

    Основные техники narrowing

    | Техника | Пример | Когда использовать | | --- | --- | --- | | typeof | typeof x === "string" | примитивы: string, number, boolean, bigint, symbol, undefined | | instanceof | err instanceof Error | классы и ошибки | | in | "id" in obj | проверка наличия поля | | сравнение литералов | status === "ok" | discriminated unions | | пользовательский type guard | function isUser(x): x is User | сложные проверки |

    Справка: Narrowing

    Discriminated unions: самый сильный паттерн для состояний

    Вместо того чтобы хранить полузаполненные объекты и проверять «что-то там есть», моделируйте состояние явно.

    Типы и имена здесь вместе создают читаемый и безопасный API.

    Когда типы начинают вредить

    TypeScript улучшает качество, пока типы остаются инструментом ясности. Красные флаги:

  • типы сложнее, чем код, который они описывают;
  • появляются длинные цепочки conditional types ради «умности»;
  • приходится постоянно использовать as, чтобы «успокоить компилятор»;
  • типы скрывают смысл вместо того, чтобы его проявлять.
  • Практика:

  • сначала сделайте понятную модель данных;
  • затем добавляйте типовые усиления ровно там, где они уменьшают риск ошибок.
  • Мини-чеклист типобезопасного дизайна

  • Включён strict.
  • На границах используется unknown, внутри — точные типы.
  • Ожидаемые ошибки моделируются как значения: union или Result.
  • Состояния моделируются через discriminated unions.
  • Дженерики применяются для переиспользования без потери типов.
  • any используется только как осознанная и редкая мера.
  • Типы улучшают читаемость, а не превращают код в «типовую акробатику».
  • !Сравнительная шпаргалка: какие решения в типах повышают качество, а какие размывают контракты

    4. Архитектура приложений: модули, слои, SOLID, зависимости, паттерны в JS/TS

    Архитектура приложений: модули, слои, SOLID, зависимости, паттерны в JS/TS

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

    Связь с предыдущими темами курса:

  • Из темы про читаемость берём принцип: смысл должен быть виден по структуре. Архитектура делает эту структуру системной.
  • Из темы про надёжный JavaScript берём идею границ: ошибки и I/O обрабатываются на краях, а внутри — чистая доменная логика.
  • Из темы про TypeScript глубоко берём контракты и моделирование состояний. Архитектура помогает правильно разместить эти контракты по слоям и модулям.
  • !Диаграмма показывает слои и направление зависимостей

    Что такое хороший архитектурный результат

    Хорошая архитектура обычно даёт следующие свойства:

  • Локальность изменений: изменение в одном месте минимально затрагивает остальные.
  • Тестируемость: бизнес-правила тестируются без реальной сети, базы и UI.
  • Ясные границы: понятно, где доменная логика, где I/O, где обработка ошибок, где преобразование данных.
  • Контролируемые зависимости: верхние уровни не знают деталей нижних, а зависят от контрактов.
  • Масштабируемость команды: разные люди могут работать в разных частях, не конфликтуя постоянно.
  • Важно: хорошая архитектура — это не максимальная абстракция. Это минимально достаточная структура под текущую сложность и ожидаемый рост.

    Модули в JS/TS: базовый строительный блок

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

    ESM как основной формат модулей

    Современный стандарт — ECMAScript Modules:

  • export делает часть модуля публичной.
  • import фиксирует зависимость явно.
  • Справка:

  • MDN: JavaScript modules
  • Практические правила качества для модулей:

  • Экспортируйте минимально необходимое. Чем меньше публичный API, тем проще сопровождение.
  • Не делайте глубокие импорты в чужие внутренности: import x from "features/payments/internal/x" создаёт хрупкие связи.
  • Старайтесь, чтобы модуль можно было понять по одному файлу входа: обычно это index.ts или явный публичный API.
  • Изоляция и циклические зависимости

    Циклические зависимости (A импортирует B, B импортирует A) ухудшают предсказуемость:

  • сложнее понять порядок инициализации;
  • часть значений может быть undefined в момент импорта;
  • растёт вероятность скрытых багов.
  • Практика:

  • выносите общие типы и контракты в отдельный модуль;
  • разрывайте циклы через зависимости на интерфейсы и передачу зависимостей параметрами;
  • избегайте выполнения сложной логики на уровне модуля при импорте.
  • Баррел-файлы: польза и риск

    Баррел-файл — это модуль, который реэкспортирует другие:

    Плюсы:

  • проще импортировать публичный API;
  • меньше шума в местах использования.
  • Риски:

  • можно случайно сделать внутренние вещи публичными;
  • иногда усложняется tree-shaking и анализ зависимостей.
  • Если используете баррелы, держите правило: баррел отражает только публичный API.

    Слои: как разделять ответственность

    Слои — это способ разместить код по типу ответственности. Упрощённая, но практичная схема для многих приложений:

  • Domain — бизнес-смысл и инварианты.
  • Application — сценарии использования, оркестрация.
  • Infrastructure — БД, сеть, файловая система, внешние SDK.
  • UI/Delivery — HTTP-контроллеры, CLI, компоненты UI.
  • Ключевой принцип: домен не должен зависеть от инфраструктуры.

    Domain: бизнес-правила и типы, которые выражают смысл

    Domain содержит:

  • сущности и value objects;
  • бизнес-правила;
  • доменные ошибки;
  • контракты для зависимостей, если они нужны домену.
  • Пример: деньги как целое число, как из темы про edge cases:

    Domain должен быть максимально чистым: минимум побочных эффектов и I/O.

    Application: use cases и оркестрация

    Application отвечает за то, как выполняется сценарий:

  • какие шаги и в каком порядке;
  • какие зависимости использовать;
  • как интерпретировать ожидаемые ошибки.
  • Здесь удобно использовать подход из темы про надёжность: ожидаемые ошибки как значения.

    Обратите внимание:

  • createOrder не делает реальных HTTP-запросов и не знает про SQL.
  • зависимости передаются через параметр deps.
  • ошибки, которые являются нормальной частью сценария, возвращаются через Result.
  • Infrastructure: детали I/O и адаптеры

    Infrastructure содержит реализации интерфейсов:

  • репозитории для базы;
  • HTTP-клиенты;
  • интеграции;
  • логирование, метрики.
  • Смысл: инфраструктура зависит от приложения или домена, потому что реализует их контракты.

    UI/Delivery: граница и преобразование в протокол

    Это слой, который превращает доменные исходы в конкретный протокол: HTTP-ответ, UI-состояние, CLI-вывод.

    Именно на границе уместны:

  • маппинг ошибок на статусы;
  • логирование;
  • рантайм-валидация входных данных;
  • корреляционные идентификаторы.
  • SOLID в JS/TS: как применять без «религии»

    SOLID — набор принципов, которые помогают снижать связность и делать код расширяемым. В JS/TS их стоит воспринимать как диагностику проблем, а не как требование «везде классы».

    Справка:

  • Wikipedia: SOLID
  • SRP: принцип единственной ответственности

    SRP: модуль или функция должны иметь одну осмысленную причину для изменения.

    Практические признаки нарушения:

  • функция одновременно валидирует данные, делает запросы и форматирует UI;
  • модуль является «свалкой» утилит без общей темы;
  • изменения в протоколе API требуют менять доменные расчёты.
  • Решение обычно архитектурное: разделить на слои и отделить вычисления от эффектов, как мы делали в теме про чистые функции.

    OCP: открытость для расширения, закрытость для модификации

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

    В JS/TS это часто достигается:

  • стратегиями;
  • таблицами соответствий;
  • композиционными обработчиками.
  • Пример стратегии расчёта скидки:

    Добавить новую скидку можно добавлением новой стратегии, не меняя applyDiscount.

    LSP: подстановка Лисков

    Если тип B является подтипом A, то B должен быть взаимозаменяем с A без сюрпризов.

    В JS/TS чаще всего это проявляется не в наследовании, а в реализациях интерфейсов:

  • репозиторий в памяти и репозиторий в базе должны одинаково соблюдать контракт;
  • мок для тестов должен вести себя как реальный сервис в важных аспектах.
  • Если реализация нарушает ожидания, это приводит к скрытым багам в use case.

    ISP: разделение интерфейсов

    Лучше иметь несколько маленьких интерфейсов, чем один «комбайн». Это снижает связанность.

    Плохой сигнал:

  • интерфейс требует реализовать методы, которые не нужны клиенту;
  • тесты вынуждены мокать лишнее.
  • В TS это решается выделением узких контрактов:

    DIP: инверсия зависимостей

    Смысл: высокоуровневый код не должен зависеть от низкоуровневых деталей напрямую. Оба должны зависеть от абстракций.

    В TS это обычно означает:

  • интерфейс в домене или приложении;
  • реализация в инфраструктуре;
  • связывание в одном месте, часто это называют composition root.
  • !Схема показывает DIP и место сборки зависимостей

    Управление зависимостями: как делать связи явными

    Dependency injection без фреймворков

    В JS/TS часто достаточно простого подхода:

  • передавать зависимости параметром deps;
  • создавать зависимости в отдельном модуле сборки.
  • Плюсы:

  • легко тестировать;
  • видно, что именно нужно use case;
  • минимум магии.
  • Границы и анти-коррупционный слой

    Если вы интегрируетесь с внешним API, не позволяйте его моделям «протечь» в домен.

    Практика:

  • внешний формат живёт в инфраструктуре;
  • доменный формат живёт в домене;
  • между ними адаптер.
  • Это уменьшает стоимость изменений: если внешний API поменялся, доменная часть остаётся стабильной.

    Конфигурация как зависимость

    Конфигурация (env, URL-ы, ключи) — тоже зависимость. Не тащите её в домен.

    Правило:

  • домен не читает process.env и не знает про окружение;
  • конфигурация читается на границе и передаётся вниз как данные.
  • Справка по переменным окружения Node.js:

  • Node.js documentation: process.env
  • Паттерны проектирования, которые реально полезны в JS/TS

    Паттерны ценны не названиями, а тем, что дают стандартные решения типовых проблем. Ниже — практичные паттерны, которые часто повышают качество в JS/TS.

    Strategy: заменяет ветвление и поддерживает OCP

    Подходит, когда:

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

    Adapter: защита домена от внешних форматов

    Подходит, когда:

  • внешний SDK имеет неудобный интерфейс;
  • внешний API нестабилен;
  • вам нужно преобразование данных.
  • Здесь адаптер изолирует:

  • формат запроса;
  • ошибки провайдера;
  • семантику ответа.
  • Repository: отделяет домен от хранения

    Подходит, когда:

  • вы хотите тестировать без реальной базы;
  • есть несколько реализаций хранения;
  • нужна единая точка доступа к агрегату.
  • Важно: репозиторий не должен превращаться в «бог-объект» для любых запросов. Сохраняйте фокус на доменной модели.

    Factory: явное создание сложных объектов

    Подходит, когда:

  • создание сущности требует валидации;
  • нужен единый способ создать корректный объект.
  • В TS factory часто лучше делать функцией, а не классом.

    Command: единица действия и логирование

    Подходит, когда:

  • нужно логировать, повторять, откатывать действия;
  • есть очередь задач;
  • важна трассировка.
  • В JS/TS команда часто выражается объектом с методом execute или просто функцией с метаданными.

    Типичная структура проекта: практичный шаблон

    Универсальной структуры нет, но можно начать с простой схемы, которая хорошо масштабируется:

  • src/domain/
  • src/application/
  • src/infrastructure/
  • src/ui/ или src/delivery/
  • src/shared/ для действительно общих вещей
  • Принципы, чтобы структура работала:

  • Доменные типы и функции не импортируют из инфраструктуры.
  • Application может импортировать домен.
  • Infrastructure импортирует домен и application, чтобы реализовать их контракты.
  • UI импортирует application и собирает зависимости.
  • Архитектурные анти-паттерны, которые часто портят качество

    «Сервисный слой» без смысла

    Симптом:

  • всё называется SomethingService;
  • функции делают «всё понемногу»;
  • нет явных use case и доменной модели.
  • Лечение:

  • назвать сценарии как use case (createOrder, cancelSubscription);
  • разделить чистые вычисления и эффекты;
  • выделить контракты на зависимости.
  • Протекание инфраструктуры в домен

    Симптом:

  • доменная функция принимает Request или Response;
  • доменные типы содержат поля, нужные только базе или UI.
  • Лечение:

  • DTO на границе;
  • маппинг и адаптеры;
  • доменные типы выражают бизнес-смысл, а не формат хранения.
  • Слишком ранняя абстракция

    Симптом:

  • интерфейсы создаются «на всякий случай»;
  • много уровней проксирования;
  • чтение кода усложняется без явной выгоды.
  • Практика:

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

    Признаки, что текущая структура мешает качеству:

  • любое изменение требует правок в десятках файлов;
  • тесты трудно писать без поднятия базы и сети;
  • код-ревью постоянно упирается в вопрос «куда это положить»;
  • количество багов от асинхронности и ошибок интеграций растёт;
  • появляется страх рефакторинга.
  • Архитектура — это продолжение всех предыдущих тем: она делает читаемость масштабируемой, надёжность управляемой, а TypeScript-контракты размещает там, где они дают максимальную пользу.

    Стабильная цель: доменная логика остаётся чистой и проверяемой, инфраструктура заменяемой, а границы — явными.

    5. Качество в продакшене: тесты, рефакторинг, линтинг, производительность, безопасность

    Качество в продакшене: тесты, рефакторинг, линтинг, производительность, безопасность

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

  • из читаемости и стиля берём понятность, маленькие функции, чистые вычисления;
  • из надёжного JavaScript — дисциплину ошибок, асинхронности, edge cases;
  • из TypeScript — контракты, строгие типы, корректное моделирование состояний;
  • из архитектуры — границы, слои, управление зависимостями.
  • Дальше — конкретные инструменты и процессы, которые делают это устойчивым.

    !Диаграмма показывает, как автоматические проверки образуют защитную сетку до продакшена

    Что значит качество в продакшене

    Качество в продакшене — это свойства системы, которые проявляются в реальных условиях:

  • корректность: фича работает по контракту, ошибки обрабатываются на границах;
  • изменяемость: изменения в одном месте не ломают другие (локальность изменений);
  • наблюдаемость: понятно, что происходит (логи, метрики, трассировка);
  • производительность: система укладывается в ограничения времени ответа, памяти, CPU;
  • безопасность: пользовательские данные защищены, атаки учитываются, зависимости контролируются.
  • Практический принцип: мы не надеемся на внимательность человека, мы строим автоматические барьеры.

    Тесты: что проверять и как не утонуть

    Тесты в JS/TS — это не про "покрытие ради покрытия", а про снижение риска изменений и предсказуемость поведения. Хорошие тесты привязаны к архитектуре: чем чище доменная логика и чем яснее границы, тем проще тестировать.

    Пирамида тестирования и скорость обратной связи

    Тесты отличаются стоимостью запуска и диагностикой.

    | Уровень | Что проверяет | Скорость | Типичные инструменты | | --- | --- | --- | --- | | Unit | функции, модули, доменные правила | высокая | Vitest, Jest | | Integration | взаимодействие компонентов, репозиториев, API-клиентов | средняя | Jest/Vitest + тестовая БД/моки, Testcontainers | | E2E | сценарий как пользователь (UI/HTTP целиком) | низкая | Playwright, Cypress |

    Практика качества:

  • быстрые юнит-тесты дают уверенность в рефакторинге;
  • интеграционные тесты ловят ошибки контрактов между слоями;
  • e2e оставляют минимальным набором критических сценариев.
  • Как писать тесты так, чтобы они помогали

    Сильный тест:

  • проверяет поведение, а не детали реализации;
  • читабелен как спецификация;
  • детерминированный (не зависит от времени, сети, случайности без фиксации);
  • быстро падает и понятно объясняет причину.
  • Популярная структура — Arrange / Act / Assert.

    Этот тест легко переживёт рефакторинг внутренней реализации calculateTotalCents, пока контракт сохраняется.

    Моки: где уместны, а где ломают качество

    Мок — это подмена зависимости.

  • Мок полезен на границах: сеть, БД, время, случайность.
  • Мок вреден, если вы начинаете мокать собственную доменную логику и проверять только "что вызвали метод".
  • Практическое правило:

  • домен тестируем как чистые функции и модели;
  • инфраструктуру тестируем через интеграцию (например, реальный SQL в контейнере или тестовый стенд);
  • use case тестируем с фейковыми реализациями интерфейсов (простыми, но реалистичными).
  • Контракты и схемы: тесты плюс рантайм-валидация

    TypeScript фиксирует контракт на этапе компиляции, но не проверяет входные данные в рантайме. На продакшен-границах полезна схемная валидация.

    Инструменты для рантайм-схем:

  • Zod
  • Valibot
  • Пример с Zod на HTTP-границе:

    Качество здесь в том, что грязный внешний мир не протекает внутрь доменной логики.

    Покрытие тестами: метрика, которой нельзя руководствоваться в одиночку

    Покрытие помогает заметить "пустые" зоны, но не гарантирует качество.

    Плохой сценарий:

  • покрытие 90%, но тесты проверяют только "что функция вызвана".
  • Хороший сценарий:

  • покрытие ниже, но тесты защищают ключевые бизнес-инварианты и критические сценарии.
  • Официальные справочники:

  • Vitest Guide
  • Jest Getting Started
  • Playwright Docs
  • Рефакторинг: как менять код без страха

    Рефакторинг — это улучшение внутренней структуры без изменения внешнего поведения. В продакшене важно, чтобы он был безопасным и дешёвым.

    Что делает рефакторинг безопасным

    Безопасность рефакторинга строится на трёх опорах:

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

  • Зафиксировать текущее поведение (тестом или хотя бы воспроизводимым сценарием).
  • Сделать маленькое изменение.
  • Запустить проверки: tsc, линт, тесты.
  • Повторить.
  • Техники, которые чаще всего окупаются в JS/TS

  • Вынос чистых вычислений из обработчиков I/O (как в теме про чистые функции).
  • Уменьшение вложенности через guard clauses.
  • Замена "флагов" в параметрах на выразительные функции или discriminated unions.
  • Декомпозиция "толстых" модулей по сценариям use case.
  • Упрощение типов: меньше as, больше честных контрактов.
  • Пример: из неявного результата в явный Result.

    Качество: код, который использует parsePositiveInt, вынужден обработать оба исхода.

    Линтинг и автоформатирование: договорённости как код

    Линтер — это автоматический код-ревьюер, который ловит ошибки до человека. Форматтер снимает бессмысленные споры на ревью.

    Базовый набор

  • Prettier: единое форматирование.
  • ESLint: правила качества, потенциальные баги.
  • TypeScript (tsc): проверка типов в CI как отдельный шаг.
  • Официальные сайты:

  • Prettier
  • ESLint
  • TypeScript
  • ESLint для TypeScript

    Для TS обычно подключают:

  • typescript-eslint
  • Пример минимальной идеи конфигурации (упрощённо, как направление):

    Правила, которые часто предотвращают реальные инциденты

  • запрет "забытых" промисов и непойманных ошибок;
  • запрет неиспользуемых переменных и параметров;
  • запрет неявных any (на уровне TS);
  • контроль циклических импортов и неправильных зависимостей между слоями.
  • Важно: линтер должен быть настроен так, чтобы команда не выключала правила массово. Лучше меньше правил, но реально выполняемых.

    Производительность: сначала измеряем, потом ускоряем

    Производительность — это не про "самый быстрый код", а про соблюдение нужных ограничений в реальном сценарии.

    Главные принципы

  • Не оптимизируйте вслепую: сначала профиль.
  • Оптимизируйте только то, что реально влияет на пользователя или стоимость инфраструктуры.
  • Оптимизация без тестов и типов повышает риск регрессий.
  • Что измерять

    В зависимости от контекста:

  • UI: время загрузки, TTI, отзывчивость, размер бандла.
  • API: p95/p99 latency, количество запросов к БД/внешним сервисам, использование памяти.
  • Node.js: event loop delay, CPU hotspots.
  • Полезные инструменты:

  • Chrome DevTools Performance
  • Lighthouse
  • Node.js Diagnostics
  • Минимальная техника измерения в коде

    В браузере:

    В Node.js:

    Качество здесь в дисциплине: время — это данные, а не ощущение.

    Частые источники проблем в JS/TS

  • лишние запросы (N+1) и отсутствие батчинга;
  • отсутствие таймаутов и отмены в I/O (связь с темой про надёжность);
  • чрезмерная параллельность: Promise.all без ограничения может перегрузить сервис;
  • тяжёлые вычисления в main thread UI;
  • рост памяти из-за кэшей без политики очистки.
  • Практики:

  • ограничивать параллельность, когда обращаетесь к внешним сервисам;
  • ставить таймауты, использовать отмену;
  • держать чистые вычисления отдельно, чтобы их можно было оптимизировать и тестировать.
  • Безопасность: базовые привычки, которые предотвращают катастрофы

    Безопасность в JS/TS проектах — это в первую очередь контроль границ: входных данных, зависимостей, секретов и прав доступа.

    Модель угроз как простая проверка здравого смысла

    Перед тем как писать код, задайте вопросы:

  • кто источник данных и можно ли ему доверять;
  • какие данные считаются чувствительными;
  • что будет, если злоумышленник подменит вход;
  • что будет, если запрос повторится много раз;
  • что будет, если зависимость окажется уязвимой.
  • Хорошая база по типам атак:

  • OWASP Top 10
  • Валидация входных данных

    Смысл: всё внешнее — подозрительно.

  • валидируйте body, query, params, заголовки;
  • не полагайтесь на TypeScript типы для внешнего ввода;
  • на ошибке возвращайте понятный ответ, но не раскрывайте внутренности.
  • Это напрямую продолжает идеи из тем про unknown и про границы архитектуры.

    XSS, инъекции и экранирование

  • В UI не вставляйте пользовательский HTML без необходимости.
  • Не делайте "сборку запросов" через конкатенацию строк.
  • Если вы работаете с базой, используйте параметризованные запросы через драйвер/ORM.

    CSRF и аутентификация

    Если у вас cookies-сессии, CSRF становится актуальным. Обычно применяют:

  • CSRF-токены;
  • SameSite cookies;
  • корректные CORS-настройки.
  • Документация по cookies и SameSite:

  • MDN: Set-Cookie
  • Уязвимости зависимостей

    Для Node.js экосистемы это критично: большая часть кода приходит из npm.

    Минимальный процесс:

  • фиксируйте lockfile (package-lock.json, pnpm-lock.yaml, yarn.lock);
  • регулярно обновляйте зависимости;
  • проверяйте уязвимости в CI.
  • Инструмент по умолчанию:

  • npm audit
  • Секреты и конфигурация

    Никогда не храните:

  • ключи API;
  • приватные токены;
  • пароли;
  • в репозитории. Используйте переменные окружения и секрет-хранилища.

    Справка:

  • Node.js process.env
  • Сборка качества в единый процесс

    Хороший продакшен-процесс — это согласованная система:

  • локально: форматирование, линт, быстрые тесты;
  • в PR: обязательные проверки и понятный код-ревью;
  • в CI: tsc, линт, тесты, сборка, проверка уязвимостей;
  • перед деплоем: минимальные e2e и smoke-тесты;
  • после деплоя: мониторинг ошибок и метрик.
  • Ключевая связка с предыдущими статьями курса:

  • читаемость снижает стоимость тестов и ревью;
  • надёжность дисциплинирует ошибки и асинхронность;
  • TypeScript фиксирует контракты и делает рефакторинг предсказуемым;
  • архитектура создаёт границы, по которым удобно тестировать и защищаться.
  • Продакшен-качество — это не одна техника, а устойчивый конвейер, в котором ошибки ловятся как можно раньше и как можно дешевле.