{ "index": 297, "slug": "editorial-2019-10-practice-typescript-migration", "title": "Переход на TypeScript: одна проверяемая граница за шаг", "excerpt": "Пошаговый способ перевести один контракт данных на TypeScript: сохранить существующую сборку, проверить внешний JSON и не прятать ошибки за массовым any.", "contentHtml": "

Симптом неудачной миграции виден в pull request: десятки файлов получают расширение .ts, ошибки компилятора закрываются через any, а команда всё ещё не знает, проверяется ли ответ API. Цена ошибки — не только большой diff. Сборка может завершиться успешно, пока экран получает объект без обязательного поля. Дефект обнаружится в браузере или у пользователя, а точка, где исчезла гарантия, уже потеряна.

\n

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

\n

Что именно нужно сохранить

\n

До изменения зафиксируйте текущий рабочий путь. Запишите команду сборки, папку её результата и короткий smoke-сценарий. Для фронтенда это может быть открытие страницы, загрузка карточки и отправка формы. Такой baseline нужен не для отчётности. Он отделяет проблему миграции от случайного изменения сборщика, формата модулей или маршрута импорта.

\n

Не смешивайте в одной итерации четыре изменения: переименование файла, новый module format, замену bundler и строгие настройки всего репозитория. Если после этого перестанет работать импорт, вы не узнаете, какая перемена стала причиной. Первая задача должна оставить старый runtime-путь и изменить только один проверяемый контракт.

\n
\"Постепенный
Переход идёт от внешней границы к следующему модулю. Сборка и пользовательский сценарий остаются отдельными контрольными точками.
\n

Граница данных важнее процента файлов

\n

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

\n
Выбор первой границы миграции
КандидатРиск без проверкиМинимальный контрактПервое действие
Ответ APIЭкран читает отсутствующее полеid, email, statusНормализовать вход перед рендером
Параметры формыСтрока уходит в команду как неверное значениеСостояние формы и команда отправкиСобрать явный объект команды
КонфигурацияПустой ключ может остановить запускОбязательные строки и допустимые значенияПроверить объект в загрузчике
Внутренний helperОшибка редко пересекает границуЛокальные аргументыОставить на следующую очередь
\n

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

\n

Сохраняем JavaScript в сборке

\n

В учебном примере ниже проект может включать оба расширения. allowJs позволяет включить существующие JavaScript-файлы рядом с TypeScript. noEmit оставляет выпуск за текущей production-сборкой. Это не готовый конфиг для любого проекта. Значения target, module и include должны соответствовать вашему runtime и структуре исходников.

\n
{ \"compilerOptions\": { \"target\": \"es5\", \"module\": \"commonjs\", \"allowJs\": true, \"checkJs\": false, \"noEmit\": true }, \"include\": [\"src/**/*\"] }
\n

На первом шаге не включайте checkJs во всём старом дереве без оценки объёма. Этот флаг сообщает об ошибках в JavaScript-файлах, которые входят в проект. Для точечного старта добавьте // @ts-check в один выбранный файл. Так вы получите ограниченный список диагностик и не превратите миграцию в инвентаризацию всего исторического долга.

\n

Проверяем внешний объект до типизированного кода

\n

Ниже — учебный пример. Он не описывает конкретный API и не утверждает, что такой ответ уже существует в рабочей системе. Функция принимает unknown, проверяет нужные поля и возвращает объект только после успешной проверки. Значение с отсутствующим email не проходит дальше.

\n
type Account = { id: string; email: string; status: \"active\" | \"blocked\" }; function isAccount(value: unknown): value is Account { if (!value || typeof value !== \"object\") return false; const record = value as Record<string, unknown>; return typeof record.id === \"string\" && typeof record.email === \"string\" && (record.status === \"active\" || record.status === \"blocked\"); } export function normalizeAccount(value: unknown): Account | null { return isAccount(value) ? value : null; } const missingEmail: unknown = { id: \"42\", status: \"active\" }; normalizeAccount(missingEmail); // null
\n

Здесь есть важная отрицательная ветка. Если сервер вернёт status: \"archived\", функция вернёт null. Дальше приложение должно явно решить, что показывать: сообщение об ошибке, безопасное состояние или повторный запрос. Это решение нельзя заменить утверждением типа. Если заменить unknown на any, компилятор разрешит читать поля без доказательства, и граница снова станет невидимой.

\n

Когда новый TypeScript-модуль импортирует старый JavaScript, типы не делают старый код runtime-безопасным. Они описывают отношения, которые compiler может проверить в исходниках. Сеть, local storage, DOM и callback сторонней библиотеки остаются внешними входами. Их проверяют там, где они входят в собственный контракт.

\n

Симптом → причина → проверка → действие

\n
Диагностика постепенной миграции
СимптомПричинаПроверкаДействие
Сотни ошибок после включения проверкиcheckJs включили на всё деревоСравнить список файлов и размер диагностикиОставить локальный @ts-check и выделить отдельную очередь
Сборка прошла, карточка падает на полеТип описал JSON, но не проверил егоПередать fixture без обязательного поляДобавить runtime-нормализатор и отрицательный тест
Ошибки исчезли после добавления anyНеизвестность перенесли в следующий модульНайти переходы any через границуЗаменить их на контракт или явно записанный временный долг
После переименования сломался импортОдновременно изменился путь или формат модулейСравнить emitted output и baseline-сборкуВернуть лишнюю перемену в отдельный шаг
Команда считает прогресс по расширениямМетрика не связана с даннымиДля каждого файла назвать вход, контракт и получателяСчитать только подтверждённые границы
\n

Порядок первого выпуска

\n
  1. Запишите действующую команду сборки, ожидаемый артефакт и один smoke-сценарий. Зафиксируйте их до изменения.
  2. Выберите одну границу: ответ API, форму или конфигурацию. Перечислите только поля, от которых зависит текущий сценарий.
  3. Добавьте отрицательную fixture: неполный объект, неверное значение enum или пустой обязательный ключ. Зафиксируйте ожидаемый отказ.
  4. Подключите TypeScript к существующему дереву через allowJs и безопасный режим вывода. Проверьте, что compiler действительно видит выбранный файл.
  5. Включите @ts-check в одном JavaScript-файле или переведите одну функцию в .ts. Не закрывайте новую диагностику массовым any.
  6. Запустите type-check, затем обычную production-сборку и smoke-сценарий. Успехом считайте только набор всех трёх проверок.
  7. Сохраните границу и способ отката. Следующий модуль добавляйте после того, как понятно, где проверяется первый.
\n

Ограничения и отрицательный путь

\n

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

\n

Не обещайте полную строгую типизацию по числу переименованных файлов. Не называйте production-готовым учебный tsconfig.json или пример нормализатора. Не включайте строгие флаги на весь репозиторий, если не оценили объём исправлений и не подготовили путь возврата. Массовый any также не является планом отката: он оставляет код исполняемым, но убирает полезную проверку.

\n

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

\n

Проверяемый критерий готовности

\n

Первая итерация готова, если команда может показать четыре факта: существующая сборка и smoke-сценарий проходят; выбранный файл входит в type-check; неполный или неверный вход не попадает в типизированный модуль; изменение можно откатить без массового восстановления дерева. Число файлов с расширением .ts в этот критерий не входит.

\n

Проверяемые источники

\n" }