В iziwork у нас стояла давняя задача: перенести кодовую базу Flow на TypeScript. Мы рады сообщить, что после нескольких месяцев подготовки мы успешно перенесли наше ядро ​​LoC объемом 300 КБ на TypeScript, не замораживая кодовую базу!

В этом сообщении блога мы увидим, что мы сделали, чтобы сделать этот переход возможным. Это первая часть из 2 частей.

Покидая поток

Когда iziwork только начинался, мы хотели воспользоваться инструментами проверки типов. Мы выбрали Flow. Забегая вперед, 3 года спустя, я думаю, можно с уверенностью сказать, что Flow потерял свою динамику. Даже Facebook, стоящий за Flow, перешел на TypeScript. TypeScript теперь широко используется в отрасли, экосистема типов намного богаче, а инструментарий TypeScript эволюционировал.

Более того, большинство наших проектов в iziwork уже использовали TypeScript. Пришло время перенести наше ядро ​​на TypeScript.

Цели

Миграция - сложная задача, и у нас были четкие цели:

  • Переносите как можно быстрее без ущерба для качества
  • Убедитесь, что в коде не будет никаких потенциально критических изменений, работая как можно больше на уровне типов. Следовательно, фактический скомпилированный код времени выполнения останется прежним.
  • iziwork идет быстро, мы не хотели замораживать кодовую базу. Перенос нужно производить параллельно
  • Автоматизируйте то, что можно автоматизировать, максимально избегая ручной работы (например, jscodeshift)

Это были цели, которые соответствовали нашей ситуации в то время. Остальная часть этого сообщения в блоге объясняет, что мы сделали, чтобы не отставать от этих целей.

Инкрементальная миграция VS миграция большого взрыва

Возможны два пути миграции:

1. Постепенно переходите с Flow на TypeScript, используя в коде файлы двух типов. Это может сработать, если использовать возможности переопределения Babel 7.

  • Легко просмотреть
  • Процесс миграции можно легко выполнить параллельно и занять недели / месяцы без необходимости замораживания кода.
  • Повторное использование типов затруднено. Мы не можем импортировать тип потока в TS, и наоборот.
  • Опыт разработки IDE ужасен. Два инструмента для проверки типа кода, слишком много информации в контекстных меню и т. Д.

2. Перенести весь код за один проход

  • Обзор может быть довольно сложным
  • Сложно обойтись без замораживания кодовой базы (предупреждение о спойлере - это выполнимо)
  • Опыт разработки довольно приятный, среда IDE должна обрабатывать только TypeScript.
  • Мы можем полностью избавиться от инструментария Flow

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

Обеспечение единообразия стиля кода

Чтобы обеспечить плавную миграцию, мы хотим, чтобы при переносе кода были чистые различия. Это упростит просмотр различных коммитов.

Кроме того, мы будем запускать инструменты автоматического переноса, скрипты и выполнять довольно много массовых операций по поиску и замене. Часто это портит наш стиль кода и в конечном итоге загрязняет нашу историю Git.

По этой причине важно использовать инструмент, который детерминированно форматирует ваш код. Мы выбрали Prettier, который в значительной степени является ведущим инструментом форматирования кода в экосистеме JS.

Вы можете выбрать нужный инструмент, но просто убедитесь, что ваш Flow-код «совместим со стилем», прежде чем начинать миграцию.

Реализация сети безопасности

В процессе миграции мы претерпим множество изменений в коде.

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

Как указано в наших целях, мы будем максимально работать над шрифтовым уровнем. Поэтому в идеале скомпилированный код JS, лишенный всех аннотаций TypeScript, должен оставаться таким же, как код Flow. Имея это в виду, у нас возникла идея сделать снимок скомпилированного JS-кода нашей кодовой базы Flow. Таким образом, при переносе кода на TypeScript мы можем сделать снимок скомпилированного кода, сравнить его с нашей точкой отсчета (снимок потока) и убедиться, что мы ничего не сломаем.

Для этого мы должны использовать тот же инструмент сборки.

В противном случае сгенерированный код будет немного другим, и его будет сложно просмотреть на тысячах файлов. Скорее всего, вы используете Babel с плагином @babel/plugin-transform-flow-strip-types. К счастью для нас, Babel поддерживает как Flow, так и TypeScript. Это означает, что мы сможем использовать Babel в качестве инструмента сборки TypeScript, а не компилятор TypeScript. Мы будем использовать tsc с опцией --noEmit, чтобы выполнять только проверку типов.

⚠️ Если вы не используете Babel для транспиляции кода Flow (например, вы можете использовать flow-remove-types), вы должны и можете легко перейти на Babel.

У Jest есть замечательная функция, которая позволяет нам делать именно то, что мы хотим: делать снимок кода и сравнивать его с эталонным снимком. Вот созданный нами тест Jest:

Вышеупомянутые тесты проверяют две вещи:

  1. Файловая структура скомпилированного кода должна оставаться прежней, сохраняя результат функции glob.
  2. Содержимое каждого файла должно оставаться неизменным.

Вооружившись этим тестом, вы теперь хотите создать свой код и запустить тест, чтобы сделать снимок файловой структуры и содержимого каждого файла. Это сгенерирует большой .snap файл, который будет эталонным снимком, используемым Jest.

💡 Совет: установите comments: false в .babelrc, прежде чем делать снимок кода.

Плагин Flow Babel может форматировать комментарии в вашем коде иначе, чем плагин TypeScript. Отключение комментариев поможет вам просмотреть реальные изменения кода вместо лишних различных форматов комментариев.

⚠️ Теперь, когда наш снимок готов, не переносите ветку миграции в ветку develop. Это сломало бы снимок. Вы сможете получить изменения из develop позже, когда вы перенесете весь код в ветку миграции.

Теперь каждый раз, когда вы меняете код в рамках миграции, вы будете проходить через этот процесс:

  1. Создайте код с помощью Babel
  2. Запустите тест сети безопасности Jest

Если он прошел, хорошо, код времени выполнения не был изменен.
Если нет, вы сможете просмотреть разницу. Если вас устраивают изменения, вы можете запустить jest --updateSnapshot, чтобы обновить снимки.

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

Запуск автоматического преобразования потока в TS

Первоначальная работа по преобразованию кода в TypeScript и сопоставлению нескольких встроенных типов Flow со встроенными типами TypeScript (например, с $Keys<T> на keyof T) может выполняться автоматически. Есть несколько инструментов, которые могут это сделать, а именно:

Первый более «известен», но он вылетал без каких-либо журналов при запуске в нашем коде.

Последний менее известен, но обеспечивает большую безопасность:

  • Он имеет больше автоматических сопоставлений типов
  • Он автоматически запускает Prettier в результирующем коде TypeScript.
  • Он проверяет, что результирующий JS AST точно такой же до и после, показывая четкие, подробные предупреждения, если это не так.

Поэтому мы решили пойти с flowts. Вы можете попробовать оба этих инструмента, ваш опыт может отличаться.

Теперь вы можете запустить npx flowts .. Теперь ваш код будет преобразован в TypeScript. Вы можете увидеть несколько предупреждений и, возможно, некоторую ошибку: ошибка проверки, сравнение после удаления аннотаций типов. Просто проверьте разницу и устраните проблемы, если они есть. В нашей кодовой базе 300 КБ у нас была одна проблема: в многострочном литерале шаблона было 2 лишних пробела. Исправить было тривиально!

Теперь наш код написан на TypeScript. Инструмент, проверяя AST, гарантирует, что выходной код такой же, и разница после удаления аннотаций типа такая же, что означает, что скомпилированный .js точно такой же. Другими словами, наш код работает, как и раньше.

Настройка инструментария для поддержки TypeScript

Теперь наш код написан на TypeScript! Пришло время настроить наши инструменты для его поддержки.

Настроить .babelrc для поддержки TypeScript довольно просто, и достаточно лишь добавить предустановку @babel/preset-typescript. Кроме того, нам нужно обновить вызовы интерфейса командной строки Babel для поддержки расширения .ts.

💡 Совет: установите onlyRemoveTypeImports: true в параметрах предустановки @babel/preset-typescript.

Плагин Flow Babel удаляет только import type импорта из сгенерированного кода. Плагин TypeScript немного умнее: он также удаляет весь импорт, который используется только как тип, даже если он явно не импортирован с синтаксисом import type. Чтобы упростить просмотр ваших снимков, отключение этого поведения с помощью вышеуказанного параметра гарантирует, что вы сохраните тот же импорт, что и раньше. Конечно, вы можете (и должны) вернуться к поведению по умолчанию после миграции.

Вам также необходимо настроить линтер и библиотеку тестирования для поддержки TypeScript. Если вы используете ESLint, следуйте инструкциям README typescript-eslint: https://github.com/typescript-eslint/typescript-eslint

Одна из самых важных частей конфигурации - это файл tsconfig.json. Поскольку мы используем Babel в качестве инструмента сборки, вам нужно настроить TypeScript только на проверку типов. Вот, например, конфигурация, которую мы используем для нашего бэкэнда в iziwork:

Обратите внимание, что мы не включили strict флаги. Строгие флаги могут легко удвоить время миграции, необходимое для tsc прохождения. Мы предлагаем постепенно включать строгие флаги после миграции.

После настройки всех инструментов не забудьте обновить свои скрипты (package.json, конвейеры CI) для запуска tsc вместо flow check. Кроме того, не забудьте настроить свою IDE для TypeScript.

Кроме того, вы можете создать свой код с помощью Babel и запустить тест сети безопасности Jest. Вы заметите, что, как гарантируется flowts, переданный код нашей версии TypeScript в точности совпадает с переданным кодом исходного кода Flow.

К настоящему времени ваши модульные тесты и CI должны пройти, за исключением проверки tsc. Успех! Ваша кодовая база и инструменты теперь поддерживают TypeScript! 🎉

Впрочем, впереди еще долгий путь ...

Бег tsc, не убегая

Когда вы запустите tsc, у вас, вероятно, будет много ошибок. Мы знаем, каково это: у нас было 20 000 ошибок.

TypeScript во многих аспектах умнее и строже, чем Flow. Но не расстраивайтесь, многие из этих первоначальных ошибок исправить очень легко:

  • Вероятно, у вас много отсутствующих @types/ пакетов
  • Возможно, в вашем коде есть псевдонимы, которых нет в tsconfig.json
  • Некоторые синтаксисы разрешены в Flow, но не в TypeScript. Например, TypeScript имеет ошибку TS1095: A 'set' accessor cannot have a return type annotation, в то время как это разрешено в Flow (с типом возврата void). Эти типы ошибок легко исправить, и их можно исправить с помощью codemods, подробнее об этом позже.

Хотя многие из этих ошибок легко исправить, трудно понять, какие именно, когда вы смотрите на загадочный tsc вывод. Давайте это исправим!

Получение более подробных отчетов об ошибках от tsc

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

В этом отчете представлены две важные данные:

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

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

Вооружившись этим, вы можете начать анализировать все свои ошибки. Вероятно, вы увидите ошибки, которые можно исправить автоматически…

Применение кодовых модов

Когда мы проанализировали наши собственные ошибки, мы обнаружили множество из них:

const foo = {}
foo.bar = true // TS2339: Property 'bar' does not exist on type '{}'

Приведенный выше код вполне допустим в Flow, но его нет в TypeScript из-за избыточной проверки свойств. Теперь, если бы у нас было всего несколько из этих ошибок, мы могли бы исправить их вручную, но у нас их было несколько тысяч. Чтобы соответствовать поведению Flow, мы должны объявить foo как any. Должен же быть способ автоматизировать это, верно?

Действительно, есть несколько инструментов, способных этого добиться. Мы выбрали:

jscodeshift - это набор инструментов, который позволяет нам создавать кодмоды. Codemod - это просто сценарий, который принимает AST в качестве входных данных, которые вы можете редактировать. Затем jscodeshift записывает полученный код обратно в исходный файл. Это намного мощнее, чем поиск / замена регулярного выражения.

Вот модификатор кода, который устраняет указанную выше проблему:

Вы можете запустить его с npx jscodeshift --parser ts --extensions ts -t ./codemod.js src.

Чтобы объяснить это вкратце:

  • Он находит все деклараторы переменных в AST
  • Для каждого из них, если у него еще нет аннотации типа, если значение является выражением объекта и если этот объект не имеет никаких свойств, мы помечаем аннотацию типа как any

Предлагаем использовать https://astexplorer.net, это действительно помогает писать codemods.

Пишите codemods только в том случае, если вы замечаете ошибки, которые систематически исправляются одним и тем же способом и которые слишком долго исправлять вручную. Нет смысла писать codemod, если для написания требуется больше времени, чем просто исправление проблемы вручную.

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

Исправление оставшихся ошибок вручную

Посмотрим правде в глаза, это самый скучный этап миграции, но и самый полезный, так как к концу вы увидите tsc успешно.

К настоящему времени вы должны иметь точное представление о том, какие еще ошибки остались. Глядя на свой индивидуальный tsc отчет, вы должны составить список ошибок, которые нужно исправить в первую очередь. Наличие 2000 ошибок не означает, что вам придется исправлять ошибки в 2000 местах; исправив ошибку где-нибудь, вы можете исправить 100 ошибок где-то еще. Вот почему так важно иметь четкий план действий.

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

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

Для справки, в iziwork этот шаг занял у нас 2 дня для 2 разработчиков, при этом около 2000 ошибок нужно было исправить вручную.

Хорошо, теперь у вас есть рабочая ветка TypeScript.

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

Будьте на связи!