Files
progcode/editorial/agent-rewrites/297.json
T
huncode 2d914b543f
Build and deploy / deploy (push) Failing after 15s
Publish rewritten technical article archive
2026-08-02 22:19:34 +03:00

8 lines
18 KiB
JSON
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{
"index": 297,
"slug": "editorial-2019-10-practice-typescript-migration",
"title": "Переход на TypeScript без остановки JavaScript-проекта",
"excerpt": "Пошаговый способ перевести один контракт данных на TypeScript: сохранить существующую сборку, проверить внешний JSON и не прятать ошибки за массовым any.",
"contentHtml": "<p>Симптом неудачной миграции виден в pull request: десятки файлов получают расширение <code>.ts</code>, ошибки компилятора закрываются через <code>any</code>, а команда всё ещё не знает, проверяется ли ответ API. Цена ошибки — не только большой diff. Сборка может сохранить зелёный статус, пока экран получает объект без обязательного поля. Дефект обнаружится в браузере или у пользователя, а точка, где исчезла гарантия, уже потеряна.</p>\n<p>Переход на TypeScript лучше начинать не с каталога файлов, а с границы данных. Выберите один внешний вход, опишите форму значения, поставьте проверку и передайте результат в новый или уже существующий модуль. При таком порядке JavaScript может оставаться частью проекта. Команда получает конкретный сигнал: неверное значение остановилось, рабочая сборка сохранилась, а новая типизация защищает реальный сценарий.</p>\n<h2>Что именно нужно сохранить</h2>\n<p>До изменения зафиксируйте текущий рабочий путь. Запишите команду сборки, папку её результата и короткий smoke-сценарий. Для фронтенда это может быть открытие страницы, загрузка карточки и отправка формы. Такой baseline нужен не для отчётности. Он отделяет проблему миграции от случайного изменения сборщика, формата модулей или маршрута импорта.</p>\n<p>Не смешивайте в одной итерации четыре изменения: переименование файла, новый module format, замену bundler и строгие настройки всего репозитория. Если после этого перестанет работать импорт, вы не узнаете, какая перемена стала причиной. Первая задача должна оставить старый runtime-путь и изменить только один проверяемый контракт.</p>\n<figure><img src=\"/assets/editorial/2019/typescript-migration-lane-2019.svg\" alt=\"Постепенный переход от JavaScript к TypeScript через проверяемую границу данных и сохранённую сборку\" loading=\"lazy\" /><figcaption>Переход идёт от внешней границы к следующему модулю. Сборка и пользовательский сценарий остаются отдельными контрольными точками.</figcaption></figure>\n<h2>Граница данных важнее процента файлов</h2>\n<p>Полезная первая граница имеет четыре свойства. Известен источник значения. Назван минимальный контракт. Есть код или тест, который отклоняет плохой вход. Понятен получатель результата. Ответ HTTP для карточки обычно подходит лучше внутреннего helper: ошибка на такой границе быстро доходит до экрана и заметна в сценарии.</p>\n<div class=\"table-scroll\"><table><caption>Выбор первой границы миграции</caption><thead><tr><th scope=\"col\">Кандидат</th><th scope=\"col\">Риск без проверки</th><th scope=\"col\">Минимальный контракт</th><th scope=\"col\">Первое действие</th></tr></thead><tbody><tr><td>Ответ API</td><td>Экран читает отсутствующее поле</td><td><code>id</code>, <code>email</code>, <code>status</code></td><td>Нормализовать вход перед рендером</td></tr><tr><td>Параметры формы</td><td>Строка уходит в команду как неверное значение</td><td>Состояние формы и команда отправки</td><td>Собрать явный объект команды</td></tr><tr><td>Конфигурация</td><td>Пустой ключ ломает запуск</td><td>Обязательные строки и допустимые значения</td><td>Проверить объект в загрузчике</td></tr><tr><td>Внутренний helper</td><td>Ошибка редко пересекает границу</td><td>Локальные аргументы</td><td>Оставить на следующую очередь</td></tr></tbody></table></div>\n<p>Тип интерфейса не проверяет данные, пришедшие по сети. JSON уже существует в runtime до того, как TypeScript увидит результат функции. Поэтому на внешнем краю нужен обычный исполняемый код: проверка типа, набора полей и допустимых значений. После неё тип помогает остальному коду не повторять те же предположения.</p>\n<h2>Сохраняем JavaScript в сборке</h2>\n<p>В учебном примере ниже компилятор видит оба расширения. <code>allowJs</code> позволяет включить существующие JavaScript-файлы рядом с TypeScript. <code>noEmit</code> оставляет выпуск за текущей production-сборкой. Это не готовый конфиг для любого проекта. Значения <code>target</code>, <code>module</code> и <code>include</code> должны соответствовать вашему runtime и структуре исходников.</p>\n<pre><code>{ \\\"compilerOptions\\\": { \\\"target\\\": \\\"es5\\\", \\\"module\\\": \\\"commonjs\\\", \\\"allowJs\\\": true, \\\"checkJs\\\": false, \\\"noEmit\\\": true }, \\\"include\\\": [\\\"src/**/*\\\"] }</code></pre>\n<p>На первом шаге не включайте <code>checkJs</code> во всём старом дереве без оценки объёма. Этот флаг сообщает об ошибках в JavaScript-файлах, которые входят в проект. Для точечного старта добавьте <code>// @ts-check</code> в один выбранный файл. Так вы получите ограниченный список диагностик и не превратите миграцию в инвентаризацию всего исторического долга.</p>\n<h2>Проверяем внешний объект до типизированного кода</h2>\n<p>Ниже — учебный пример. Он не описывает конкретный API и не утверждает, что такой ответ уже существует в рабочей системе. Функция принимает <code>unknown</code>, проверяет нужные поля и возвращает объект только после успешной проверки. Значение с отсутствующим <code>email</code> не проходит дальше.</p>\n<pre><code>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&lt;string, unknown&gt;; return typeof record.id === \\\"string\\\" &amp;&amp; typeof record.email === \\\"string\\\" &amp;&amp; (record.status === \\\"active\\\" || record.status === \\\"blocked\\\"); } export function normalizeAccount(value: unknown): Account | null { return isAccount(value) ? value : null; }</code></pre>\n<p>Здесь есть важная отрицательная ветка. Если сервер вернёт <code>status: \\\"archived\\\"</code>, функция вернёт <code>null</code>. Дальше приложение должно явно решить, что показывать: сообщение об ошибке, безопасное состояние или повторный запрос. Это решение нельзя заменить утверждением типа. Если заменить <code>unknown</code> на <code>any</code>, компилятор разрешит читать поля без доказательства, и граница снова станет невидимой.</p>\n<p>Когда новый TypeScript-модуль импортирует старый JavaScript, типы не делают старый код runtime-безопасным. Они описывают отношения, которые compiler может проверить в исходниках. Сеть, local storage, DOM и callback сторонней библиотеки остаются внешними входами. Их проверяют там, где они входят в собственный контракт.</p>\n<h2>Симптом → причина → проверка → действие</h2>\n<div class=\"table-scroll\"><table><caption>Диагностика постепенной миграции</caption><thead><tr><th scope=\"col\">Симптом</th><th scope=\"col\">Причина</th><th scope=\"col\">Проверка</th><th scope=\"col\">Действие</th></tr></thead><tbody><tr><td>Сотни ошибок после включения проверки</td><td><code>checkJs</code> включили на всё дерево</td><td>Сравнить список файлов и размер диагностики</td><td>Оставить локальный <code>@ts-check</code> и выделить отдельную очередь</td></tr><tr><td>Сборка прошла, карточка падает на поле</td><td>Тип описал JSON, но не проверил его</td><td>Передать fixture без обязательного поля</td><td>Добавить runtime-нормализатор и отрицательный тест</td></tr><tr><td>Ошибки исчезли после добавления <code>any</code></td><td>Неизвестность перенесли в следующий модуль</td><td>Найти переходы <code>any</code> через границу</td><td>Заменить их на контракт или явно записанный временный долг</td></tr><tr><td>После переименования сломался импорт</td><td>Одновременно изменился путь или формат модулей</td><td>Сравнить emitted output и baseline-сборку</td><td>Вернуть лишнюю перемену в отдельный шаг</td></tr><tr><td>Команда считает прогресс по расширениям</td><td>Метрика не связана с данными</td><td>Для каждого файла назвать вход, контракт и получателя</td><td>Считать только подтверждённые границы</td></tr></tbody></table></div>\n<h2>Порядок первого выпуска</h2>\n<ol><li>Запишите действующую команду сборки, ожидаемый артефакт и один smoke-сценарий. Зафиксируйте их до изменения.</li><li>Выберите одну границу: ответ API, форму или конфигурацию. Перечислите только поля, от которых зависит текущий сценарий.</li><li>Добавьте отрицательную fixture: неполный объект, неверное значение enum или пустой обязательный ключ. Зафиксируйте ожидаемый отказ.</li><li>Подключите TypeScript к существующему дереву через <code>allowJs</code> и безопасный режим вывода. Проверьте, что compiler действительно видит выбранный файл.</li><li>Включите <code>@ts-check</code> в одном JavaScript-файле или переведите одну функцию в <code>.ts</code>. Не закрывайте новую диагностику массовым <code>any</code>.</li><li>Запустите type-check, затем обычную production-сборку и smoke-сценарий. Успехом считайте только набор всех трёх проверок.</li><li>Сохраните границу и способ отката. Следующий модуль добавляйте после того, как понятно, где проверяется первый.</li></ol>\n<h2>Ограничения и отрицательный путь</h2>\n<p>Постепенная миграция не уменьшает автоматически количество ошибок в старом JavaScript. Она ограничивает область новой проверки. Если модуль вызывают разные страницы с несовместимыми аргументами, сначала нужно найти реальный контракт или разделить адаптеры. Один тип для всех вызовов только скроет различия.</p>\n<p>Не обещайте полную строгую типизацию по числу переименованных файлов. Не называйте production-готовым учебный <code>tsconfig.json</code> или пример нормализатора. Не включайте строгие флаги на весь репозиторий, если не оценили объём исправлений и не подготовили путь возврата. Массовый <code>any</code> также не является планом отката: он оставляет код исполняемым, но убирает полезную проверку.</p>\n<p>Если выбранная граница не даёт воспроизводимой отрицательной проверки, остановитесь. Перенос файла сам по себе не доказывает пользу. Вернитесь к контракту, найдите входное значение и сформулируйте случай, который должен быть отклонён. Если это невозможно сделать без изменения backend, сборщика или публичного API, вынесите зависимость в отдельную задачу.</p>\n<h2>Проверяемый критерий готовности</h2>\n<p>Первая итерация готова, если команда может показать четыре факта: существующая сборка и smoke-сценарий проходят; выбранный файл входит в type-check; неполный или неверный вход не попадает в типизированный модуль; изменение можно откатить без массового восстановления дерева. Число файлов с расширением <code>.ts</code> в этот критерий не входит.</p>\n<h2>Проверяемые источники</h2>\n<ul><li><a href=\"https://www.typescriptlang.org/docs/handbook/migrating-from-javascript.html\" target=\"_blank\" rel=\"noopener noreferrer\">TypeScript Handbook: Migrating from JavaScript</a> — официальный маршрут настройки <code>tsconfig</code>, работы с <code>allowJs</code> и перевода файлов по одному.</li><li><a href=\"https://www.typescriptlang.org/tsconfig/allowJs.html\" target=\"_blank\" rel=\"noopener noreferrer\">TypeScript TSConfig: allowJs</a> — официальное описание включения JavaScript-файлов в проект TypeScript.</li><li><a href=\"https://www.typescriptlang.org/tsconfig/checkJs.html\" target=\"_blank\" rel=\"noopener noreferrer\">TypeScript TSConfig: checkJs</a> — официальное описание диагностики JavaScript вместе с <code>allowJs</code> и эквивалента <code>@ts-check</code>.</li></ul>"
}