TypeScript - официальный язык веб-разработки на Airbnb. Тем не менее, процесс внедрения TypeScript и миграции зрелой кодовой базы, содержащей тысячи файлов JavaScript, произошел не за один день. Принятие TypeScript прошло через процесс первоначального предложения, принятие несколькими командами, бета-фазу и, наконец, становление официальным языком фронтенд-разработки на Airbnb. Вы можете узнать больше о том, как мы масштабно адаптировали TypeScript, в этом выступлении Бри Бандж.

Стратегии миграции

Масштабная миграция - сложная задача, и мы рассмотрели несколько вариантов перехода с JavaScript на TypeScript:

1) Стратегия гибридного переноса. Частичный перенос файла за файлом, исправление ошибок типа и повторение до тех пор, пока не будет перенесен весь проект. Параметр конфигурации allowJS позволяет нам сосуществовать в проекте бок о бок с файлами TypeScript и JavaScript, что делает этот подход возможным!

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

2) Полная миграция! Возьмите проект JavaScript или частичный проект TypeScript и полностью его преобразуйте. Нам нужно будет добавить несколько any типов и @ts-ignorecomments, чтобы проект компилировался без ошибок, но со временем мы сможем заменить их более описательными типами.

Выбор стратегии комплексной миграции дает несколько существенных преимуществ:

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

Похоже, здесь явный победитель - полная миграция! Но процесс полной миграции большой и зрелой кодовой базы - серьезная и сложная проблема. Для решения этой проблемы мы решили использовать скрипты модификации кода - codemods! В ходе первоначального процесса ручного перехода на TypeScript мы распознали повторяющиеся операции, которые можно автоматизировать. Мы сделали codemods для каждого из этих шагов и объединили их в общий конвейер миграции.

По нашему опыту, нет 100% гарантии, что автоматическая миграция приведет к полностью безошибочному проекту, но мы обнаружили, что комбинация шагов, описанных ниже, дала нам наилучшие результаты при окончательном переходе на безошибочный TypeScript. проект. Мы смогли конвертировать проекты, содержащие более 50 000 строк кода и более 1 000 файлов из JavaScript в TypeScript за один день с использованием codemods!

На основе этого конвейера мы создали инструмент под названием «ts-migrate»:

В Airbnb мы используем React для значительной части кодовой базы нашего внешнего интерфейса. Вот почему некоторые части codemods связаны с концепциями, основанными на React. ts-migrate потенциально может использоваться с другими фреймворками или библиотеками с дополнительной настройкой и тестированием.

Этапы процесса миграции

Давайте рассмотрим основные шаги, необходимые для миграции проекта с JavaScript на TypeScript, и то, как эти шаги реализованы:

1) Первая часть каждого проекта TypeScript - это создание файла tsconfig.json, и ts-migrate может сделать это, если потребуется. Существует шаблон файла конфигурации по умолчанию и проверка, которая помогает нам обеспечить единообразную настройку всех проектов. Вот пример конфигурации базового уровня:

2) После того, как файл tsconfig.json размещен, следующим шагом будет изменение расширений файлов исходного кода с .js/.jsx на .ts/.tsx. Автоматизировать этот шаг довольно просто, и он также избавляет от значительной части ручной работы.

3) Следующий шаг - запуск codemods! Мы называем их «плагины». Плагины для ts-migrate - это модули кода, которые имеют доступ к дополнительной информации через языковой сервер TypeScript. Плагины принимают строку на входе и создают обновленную строку на выходе. jscodeshift, TypeScript API, замену строки или другие инструменты модификации AST можно использовать для преобразования кода.

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

Обзор пакетов ts-migrate

Мы разделили ts-migrate на 3 пакета:

Таким образом, мы смогли отделить логику преобразования от основного бегуна и создать несколько конфигураций для разных целей. На данный момент у нас есть два основных конфига: migration и reignore.

Хотя цель конфигурации миграции - перейти с JavaScript на TypeScript, цель reignore - сделать проект компилируемым, просто игнорируя все ошибки. Reignore полезен, когда у вас большая кодовая база и вы выполняете такие задачи, как:

  • обновление версии TypeScript
  • внесение серьезных изменений или рефакторинга в кодовую базу
  • улучшение типов некоторых часто используемых библиотек

Таким образом, мы можем перенести проект, даже если есть ошибки, с которыми мы не хотим сразу разбираться. Это значительно упрощает обновление TypeScript или библиотек.

Обе конфигурации запускаются на ts-migrate-server, который состоит из двух частей:

  • TSServer: эта часть очень похожа на то, что делает редактор VSCode для связи между редактором и языковым сервером. Новый экземпляр языкового сервера TypeScript работает как отдельный процесс, а инструменты разработки взаимодействуют с сервером с помощью языкового протокола.
  • Migration runner: Эта часть запускает и координирует процесс миграции. Ожидаются следующие параметры:

И выполняет следующие действия:

  1. Разобрать tsconfig.json.
  2. Создать исходные файлы .ts.
  3. Отправлять каждый файл на сервер языка TypeScript для диагностики. Компилятор предоставляет нам три типа диагностики: semanticDiagnostics, syntacticDiagnostics и suggestionDiagnostics. Мы используем эту диагностику, чтобы найти проблемные места в исходном коде. По уникальному диагностическому коду и номеру строки мы можем определить потенциальный тип проблемы и внести необходимые изменения в код.
  4. Запустить все плагины для каждого файла. Если текст изменяется из-за выполнения плагина, мы обновляем содержимое исходного файла и уведомляем сервер языка TypeScript об изменении файла.

Вы можете найти примеры использования ts-migrate-server в пакете примеров или основном пакете. ts-migrate-example также содержит базовые примеры плагинов. Их можно разделить на 3 основные категории:

В репозитории есть набор примеров, демонстрирующих, как создавать простые плагины всех типов и использовать их в сочетании с ts-migrate-server. Вот пример конвейера миграции, который преобразует следующий код:

в:

ts-migrate выполнил 3 преобразования в приведенном выше примере:

  1. поменял местами все идентификаторы first -> tsrif
  2. добавлены типы в объявление функции function tlum(tsrif, dnoces) -> function tlum(tsrif: number, dnoces: number): number
  3. вставлен console.log(‘args:${arguments}’);

Общие плагины

Реальные плагины находятся в отдельном пакете - ts-migrate-plugins. Давайте посмотрим на некоторые из них. У нас есть два подключаемых модуля на основе jscodeshift: explicitAnyPlugin и declareMissingClassPropertiesPlugin. Jscodeshift - это инструмент, который может преобразовывать AST обратно в строку с помощью пакета recast. Используя функцию toSource(), мы можем напрямую обновлять исходный код наших файлов.

Основная идея explicitAnyPlugin заключается в извлечении всех ошибок semanticDiagnostics с сервера языка TypeScript вместе с номерами строк. Затем нам нужно будет добавить тип any в строки, указанные в диагностике. Такой подход позволяет нам устранять ошибки, поскольку добавление типа any исправляет ошибки компиляции.

До:

После:

declareMissingClassPropertiesPlugin принимает всю диагностику с кодом 2339 (вы можете догадаться, что означает этот код?), И если он сможет найти объявление класса с отсутствующими идентификаторами, плагин добавит их в тело класса с помощью any аннотация типа. Как видно по названию, этот кодмод применим только для классов ES6.

Следующая категория плагинов - это плагины на основе TypeScript AST. Анализируя AST, мы можем сгенерировать массив обновлений в исходном файле следующих типов:

После создания обновлений остается только применить изменения в обратном порядке. Если в результате этих операций мы получаем новый текст, мы обновляем исходный файл. Давайте взглянем на пару из этих подключаемых модулей на основе AST: stripTSIgnorePlugin и hoistClassStaticsPlugin.

stripTSIgnorePlugin - это первый плагин в конвейере миграции. Он удаляет все экземпляры @ts-ignore¹ из файла. Если мы конвертируем проект JavaScript в TypeScript, этот плагин ничего не сделает. Однако, если это частичный проект TypeScript (в Airbnb у нас было несколько проектов в этом состоянии), это важный первый шаг. Только после удаления @ts-ignore комментариев компилятор TypeScript выдаст все диагностические ошибки, которые необходимо устранить.

трансформируется в:

После удаления @ts-ignore комментариев мы запускаем hoistClassStaticsPlugin. Этот плагин просматривает все объявления классов в файле. Он определяет, можем ли мы поднять идентификаторы или выражения, и определяет, было ли присвоение уже поднятому классу.

Чтобы иметь возможность быстро выполнять итерацию и предотвращать регрессию, мы добавили серию модульных тестов для каждого плагина и ts-migrate.

Плагины для React

reactPropsPlugin преобразует информацию о типе из PropTypes в определение типа свойства TypeScript. Он основан на замечательном инструменте, написанном Mohsen Azimi. Нам нужно запускать этот плагин только для .tsx файлов, которые содержат хотя бы один компонент React. reactPropsPlugin ищет все PropTypes объявления и пытается проанализировать их, используя AST и простые регулярные выражения, такие как /number/, или более сложные случаи, такие как /objectOf$/. Когда компонент React (функциональный или класс) обнаруживается, он преобразуется в компонент с новым типом свойств: type Props = {…};.

reactDefaultPropsPlugin охватывает шаблон defaultProps для компонентов React. Мы используем специальный тип, представляющий реквизиты со значениями по умолчанию:

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

Понятия состояния и жизненного цикла довольно распространены в экосистеме React. Мы обратились к ним в двух плагинах. Если компонент отслеживает состояние, reactClassStatePlugin генерирует новый type State = any;, а reactClassLifecycleMethodsPlugin аннотирует методы жизненного цикла компонента с соответствующими типами. Функциональность этих подключаемых модулей может быть расширена, включая возможность замены any на более описательные типы.

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

Обеспечение успешной компиляции проекта

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

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

Последняя часть конвейера миграции гарантирует устранение всех нарушений компиляции TypeScript. Для обнаружения и исправления потенциальных ошибок tsIgnorePlugin выполняет семантическую диагностику с номерами строк и вставляет @ts-ignore комментарии с полезными пояснениями, например:

Мы также добавили поддержку синтаксиса JSX:

Наличие значимых сообщений об ошибках в комментариях упрощает исправление проблем и повторный просмотр кода, который требует внимания. Эти комментарии в сочетании с $TSFixMe² позволяют нам собирать полезные данные о качестве кода и выявлять потенциально проблемные области кода.

И последнее, но не менее важное: нам нужно дважды запустить плагин eslint-fix. Когда-то перед tsIgnorePlugin данное форматирование может повлиять на то, где мы получим ошибки компилятора. И снова после tsIgnorePlugin, поскольку вставка @ts-ignore комментариев может привести к новым ошибкам форматирования.

Резюме

Наша история миграции находится в стадии разработки: у нас есть несколько устаревших проектов, которые все еще находятся на JavaScript, и у нас все еще есть большое количество комментариев $TSFixMe и @ts-ignore в нашей кодовой базе.

Однако использование ts-migrate значительно ускорило процесс миграции и повысило производительность. Инженеры смогли сосредоточиться на улучшении набора текста вместо того, чтобы выполнять перенос файлов вручную. В настоящее время около 86% нашего 6-мегабайтного монорепозитория внешнего интерфейса было преобразовано в TypeScript, и к концу года мы выйдем на 95%.

Вы можете проверить ts-migrate и найти инструкции по установке и запуску ts-migrate в основном пакете в репозитории Github. Если вы обнаружите какие-либо проблемы или у вас есть идеи по улучшению, мы будем рады вашим вкладам!

Огромный привет Бри Бандж, которая была движущей силой TypeScript в Airbnb и создателем ts-migrate. Спасибо Джо Ленсиони за помощь в внедрении TypeScript в Airbnb и улучшение нашей инфраструктуры и инструментов TypeScript. Особая благодарность Elliot Sachs и John Haytko за вклад в ts-migrate. И спасибо всем, кто оставил отзыв и помог в пути!

Сноска

Мы хотим отметить пару вещей о миграции, которые мы обнаружили в процессе и которые могут быть полезны:

  • В версии TypeScript 3.7 были введены комментарии @ts-nocheck, которые можно добавлять вверху файлов TypeScript для отключения семантических проверок. Мы не использовали этот комментарий, поскольку раньше он не поддерживал .ts/.tsx файлы, но он также может быть отличным помощником на промежуточном этапе во время миграции.
  • В версии 3.9 TypeScript появилось @ts-expect-error комментариев. Когда строка предваряется комментарием @ts-expect-error, TypeScript подавляет сообщение об этой ошибке. Если ошибки нет, TypeScript сообщит, что @ts-expect-error в этом нет необходимости. В кодовой базе Airbnb мы перешли на использование комментария @ts-expect-error вместо @ts-ignore.

[1]: @ts-ignore комментарии позволяют нам указать компилятору игнорировать ошибки в следующей строке.

[2]: Мы ввели собственные псевдонимы для типа any - $TSFixMe и для типа функции - $TSFixMeFunction = (…args: any[]) => any;. Хотя лучше всего избегать типа any, его использование помогло нам упростить процесс миграции и прояснило, какие типы следует пересмотреть.