Как мы добавили strict-проверки TypeScript в существующий проект

Как мы добавили strict-проверки TypeScript в существующий проект

Максим Овчарик
Максим Овчарик Ведущий фронтенд-разработчик
6 февраля 2025

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

Изображение записи

Наш проект имеет долгую историю. И за это время подходы к разработке фронтенда успели несколько раз измениться. В какой-то период в проекте можно было встретить код на JavaScript, CoffeeScript и TypeScript. Плюс сам TypeScript успел обновиться несколько раз за время существования проекта со второй до пятой версии.

Сейчас TypeScript практически вытеснил весь остальной код, но процесс по переписыванию с других языков занял много времени. И чтобы была возможность переиспользовать уже написанный код в TS-модулях, было принято решение отказаться от строгих проверок. Из-за выключенных проверок в коде накопилось большое количество использований any, а также отсутствующих проверок на null. Это, наверное, основные две проблемы, которые не позволяют единовременно пройтись по всей кодовой базе и за раз исправить все ошибки, возникающие при включении строгих проверок.

В общем, если вам знакома эта ситуация, то статья для вас. Меня зовут Максим Овчарик, я ведущий фронтенд-разработчик в Selectel. В этом материале расскажу, как мы строили процесс миграции кода на строгий режим TypeScript.

TL;DR Файлы tsconfig.json настраиваются таким образом, чтобы IDE производила все необходимые проверки, а для сборки используется отдельный конфиг. Для автоматизации проверок в CI/CD используется npm пакет @selectel/ts-check — по тем же правилам, которые применяются в IDE. Инструкция по установке и использованию находится в описании пакета.

Существующие решения

Сама проблема миграции на strict-режим не новая. Например, в репозитории TS с 2018 года висит запрос на решение этой проблемы, который по-прежнему остается без окончательного ответа. Главный вопрос тут заключается в том, как включить строгие проверки только на часть файлов, а не на весь проект, чтобы можно было начать процесс постепенной миграции.

В этом же треде на GitHub можно найти одно из предлагаемых решений — включение проверок через плагин typescript-strict-plugin. Сразу хочу отметить минусы этого решения.

  • Перед началом работы необходимо все файлы, которые не удовлетворяют строгим проверкам, отметить специальным образом.
  • Необходимо использовать кастомные настройки в tsconfig.json.
  • Для автоматизации проверок потребуется сборка всего проекта с включенным плагином.

На Хабре есть пара статей от @difuks по созданию плагинов для TypeScript. И в качестве примера там рассматривается реализация очень похожего решения. Лично мне было довольно интересно ознакомиться с подробностями этого решения и с тем, как работают плагины в TypeScript.

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

Сервер TypeScript и система проектов tsconfig.json

Давайте рассмотрим, что предлагают данные плагины в качестве решения проблемы миграции на strict-режим.

Фича 1. Включение строгих проверок для части файлов в IDE.

Фича 2. Включение строгих проверок для части файлов в сборке.

Первый пункт, очевидно, критически важен для постепенной миграции на строгий режим. Но основная загвоздка в том, что TypeScript это и так умеет делать. Сервер TypeScript, который используют IDE для анализа файлов .ts, позволяет гибко настраивать дерево конфигураций tsconfig.json. Если вкратце, то сервер TypeScript будет брать ближайший файл tsconfig.json вверх по дереву директорий от проверяемого файла.

Допустим, есть следующая структура файлов:

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

Что это дает? Например, есть команда разработчиков, которая работает только над определенной частью проекта. И эта команда хочет включить подсветку ошибок строгого режима где-то в поддереве проекта. Для этого достаточно добавить файл конфигурации tsconfig.json в нужное поддерево, а в самой конфигурации указать, что она расширяет родительскую, и прописать интересующие правила. Скорее всего, любые редакторы кода и IDE не потребуют дополнительных манипуляций, чтобы выполнить проверки по новым правилам. Это действие никак не повлияет на весь остальной проект, другие команды и сборку проекта (об этом чуть позже).

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

Поиск и разработка решения

Проблема с деревом конфигураций tsconfig.json заключается в том, что они учитываются только в IDE. Компилятор TypeScript (tsc) принимает на вход только один файл конфигурации и согласно этой конфигурации собирает весь проект. В нашем случае вообще используется Angular, где инкапсулируется передача этого параметра. Заставить собирать проект согласно множеству файлов конфигурации — звучит как довольно нетривиальная задача. И, насколько я понимаю, плагины выше именно это и делают. То есть генерируют конфигурации согласно заданным правилам: для каждой конфиги создают отдельный инстанс Language Service, которому скармливают определенную часть файлов проекта. В общем, выглядит это сложно и дорого по ресурсам, особенно на больших проектах.

Когда появилась задача автоматизировать проверки строгого режима в CI/CD, моя первая мысль заключалась в том, что можно просто воспользоваться механизмом, который используют IDE. Рассуждал в этот момент примерно так:

Мысль 1. У нас нет необходимости и желания каждый раз проверять проект полностью. Достаточно проверить, что файлы, добавленные или измененные в рамках отдельной задачи, строго валидируются TypeScript. Такой же подход у нас используется для проверки правил линтера и форматирования кода. То есть при добавлении новых правил или изменении существующих мы не вносим исправления сразу в весь проект. Но ожидаем, что исправления будут постепенно вноситься разработчиками, а автоматизация позволит контролировать этот процесс. С TypeScript аналогично: язык развивается — в нем появляются новые проверки. И для нас будет удобнее, если процесс их внедрения станет итеративным.

Мысль 2. IDE не тратит много ресурсов на проверку отдельных файлов. Инициализация TypeScript-сервера занимает некоторое время, но после этого ошибки TS отображаются практически мгновенно для любого из нормальных файлов проекта. И это не сильно зависит от объема самого проекта или количества зависимостей в файле.

Мысль 3. Настройка tsconfig.json для IDE не влияет на процесс сборки проекта. Меньше всего хочется что-то менять в этом процессе, а потом еще и поддерживать эти изменения. Ведь они несут определенные риски и обычно затрагивают сразу всех разработчиков. Основная проблема тут может быть связана с тем, что правила, указанные для IDE и для сборки, окажутся несовместимыми. Но такие ситуации обычно проявляются только в частном порядке.

Мысль 4. У нас уже все настроено для проверок строгих правил в IDE. Остается только настроить CI/CD таким образом, чтобы пайплайн не пропускал код с ошибками строгого режима TypeScript в тех файлах, которые были затронуты разработчиком в рамках решения задачи.

Как оказалось, готового решения для таких проверок нет. Возможно, я плохо искал, так как само решение и его реализация лежат на поверхности. Из готового в то время удалось найти только typescript-strict-plugin, про который писал выше. Но я посмотрел исходный код — и ничего полезного для себя не обнаружил. Уже позже наткнулся на документацию к TypeScript-серверу. И там даже есть описание его API:

tsserver supports a set of commands. The full list of commands supported by the server can be found under ts.server.protocol.CommandTypes.

Each command is associated with a request and a response interface. For instance command «completions» corresponds to request interface CompletionsRequest, and response interface defined in CompletionsResponse.

 

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

По итогу получилась утилита, которая на вход принимает список .ts-файлов и запускает инстанс TypeScript-сервера. Просит сервер проверить переданный список файлов на наличие проблем,  после чего собирает ответ, форматирует его и выводит в консоль сообщения с ошибками:

Пример работы утилиты.

Готовое решение мы уже собрали в npm-пакет и опубликовали на npmjs вместе с инструкцией по установке.

Интеграция с GitLab

Утилита поддерживает вывод в формате Code Quality репортов GitLab. Поэтому для полноценной интеграции с GitLab необходима примерно следующая конфигурация задачи CI:


    ts-check:
  image: node:20-alpine
  script:
    - npm ci
    - npx ts-check --gitlab-report ts-check-report.json <paths/to/files/*.ts>
  artifacts:
    reports:
      codequality: ts-check-report.json

Через опцию —gitlab-report задается путь до файла, в который будет записан результат проверки. По итогу можно будет увидеть такой вывод в задаче CI:

Пример вывода: джоба не сработала.

Найденные ошибки будут отображаться в Code Quality виджете MR:

Пример ошибки при запросе на слияние.

В случае использования Ultimate версии GitLab ошибки будут отображаться при просмотре изменений:

GitLab, пример построчного отображения ошибок в окне просмотра изменений.

Взаимодействие с сервером TypeScript

Сбор данных по ошибкам происходит через сервер TypeScript. Он запускается в отдельном процессе. Для общения с ним пришлось написать небольшую обертку. На мой взгляд, получился довольно удобный интерфейс, построенный на потоках RxJs:


    import ts from "typescript";
import { TsServer } from "@selectel/ts-check";

const file = "test.ts";

// Запрос на открытие файла
const openRequest: ts.server.protocol.OpenRequest = {
  seq: 1,
  type: "request",
  command: ts.server.protocol.CommandTypes.Open,
  arguments: { file },
};

// Запрос на поиск ошибок в файле
const diagnosticRequest: ts.server.protocol.SemanticDiagnosticsSyncRequest = {
  seq: 2,
  type: "request",
  command: ts.server.protocol.CommandTypes.SemanticDiagnosticsSync,
  arguments: { file },
};

// Создание экземпляра сервера
const server = new TsServer();

// RxJs поток с ответами сервера
const responses$ = server.listen();

// Подписка на ответы сервера
responses$.subscribe(console.log);

// Отправка запросов серверу
server.send(openRequest);
server.send(diagnosticRequest);

С доступными командами можно ознакомиться в исходном коде сервера TypeScript, как это следует из его документации. Надеюсь, статья и наше небольшое исследование окажутся полезными для ваших проектов.