Привет, меня зовут Артур, я frontend-разработчик в компании Exness.
Недавно мы с моим коллегой Сергеем совершили перевод кодовой базы нашего сервиса с Flow.js на Typescript.

Думаю, статья будет полезна не столько своей технической составляющей, которая тоже в какой-то мере присутствует, сколько самим процессом разработки. Мы не изобретали новый подход или язык программирования, но прошли сложный путь и достигли поставленной цели. Наш случай ярко демонстрирует пример абстрактной задачи «Дано: пункт А, нужно дойти в пункт Б. Степень неопределенности высокая, срок такой-то. Поехали». 

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

Немного истории

Давайте вспомним, как и почему разработчики стали переходить на TS. Javascript - это язык с динамической типизацией, то есть в нем нет необходимости явно указывать тип переменных. И это здорово, потому что быстро и без лишней писанины. Но спустя какое-то время разработчики поняли, что хотят точно знать, какие типы параметров приходят к ним, и стали добавлять множество проверок приходящих значений. Это было болью и для разработки, и для тестирования, и для модификаций.

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

Следом приходит flow.js разработки Facebook. И это становится большим шагом на пути к «правильной» типизации. Тут вам и онлайн чекинг, и возможность ссылаться на типы и их свойства, поддержка в IDE. Но это статический анализатор. А значит идет анализ кода на правильное использование типов разработчиками. На практике оказывается, что не все типы правильно выводятся, необходимо явно указывать типы, что порождает лишнее количество текста. Вывод типов из функций и методов не всегда получается. Свой особый синтаксис. Технология развивается уже не так бурно.

Тем временем, на сцену выходит Typescript от Microsoft, который уже является полноценным компилятором кода, надстройка над Javascript, который строго проверяет типизацию. И это становится мэйнстримом - поддержка и развитие от Microsoft, богатые возможности самого компилятора по составлению хитрых и сложных типов, а некоторые умельцы даже умудряются делать сортировку списка через типизации. Интеграция в IDE на отличном уровне. Вывод типов настолько хорош, что теперь достаточно описывать их в нескольких «базовых» местах, чтобы дальше по цепочке тип вывелся и в функциях, и в классах. Привычный Java/C#синтаксис.

Удобство, мощность, мэйнстрим, низкий порог входа в проект для новых разработчиков, привычность использования, действительно хорошая защита типами от ошибки разработчика - эти причины привели к тому, что мы решили переходить на Typescript сразу, как только будут ресурсы.

Стартовая линия

Знакомьтесь: наш проект. Мне важно представить его вам для понимания, от чего мы отталкивались. Видение стартовой точки поможет понять, насколько близки вам дальнейшие наши процессы, да и для общего знания пригодится.Итак. У нас типичное React-Redux-Saga приложение, но собранное не через create-react-app утилиту, а скомпонованное самостоятельно. Для сборки мы используем webpack, для транспиляции babel, eslint в качестве линтера. У нас имеются react-hook-form, обычные компоненты от простых до сложных контейнеров, server-side rendering. Из уникальных библиотек мы используем Ramda.js и XState.js. Имеется набор наших общих компонентов, написанных на JS+Flow, которые мы импортируем отдельно. На фронтовом бэкенде express.js. Все, в общем-то, довольно таки типично. Уверен, что множество написанных на React проектов имеют схожую компоновку. И, конечно же, виновник торжества Flow.js.

Этап первый. Подготовка базы

Первым делом требовалось настроить webpack.config, eslint, babel для TS кода. Для этого я полез смотреть, как сделано в соседнем сервисе. Этот был проект был крайне похож на наш, но немного ушедший в сторону в некоторых моментах. Одно дело несколько лет работать используя TS, и совсем иное - все это настроить. Словно другой мир.

Итак, берем заготовку tsconfig, обязательно прописывая path блок, чтобы IDE понимала алиасы для импортов:

{
  "compilerOptions": {
    "allowJs": false,
    "allowSyntheticDefaultImports": true,
    "baseUrl": "src",
    "paths": {
      "types/*": [
        "../types/*"
      ],
      "typings/*": [
        "./typings/*"
      ],
      "components/*": [
        "./components/*"
      ],

}

Настраиваем обработку ts файлов в .webpack.config, добавляя в rules для babel-loader плагин '@babel/preset-typescript'.
В  babel.config в списке расширений добавляем ts/tsx файлы extensions: ['.js', '.jsx', '.ts', '.tsx', '.css']. Добавляем обработку .ts/.tsx файлов в eslint.config. 

Для jest.config.js требовалось прописать обработчик для .ts файлов и указать расширения

}
globals: {

      'ts-jest': {
      tsconfig: 'tsconfig.json',
    },
  },
  testMatch: [
    '**/__tests__/**/*test.js',
    '**/__tests__/**/*test.ts',
  ],
  moduleFileExtensions: [
    'ts',
    'tsx',
    'js',
    'jsx',
  ],
….
}

И даже для .flowconfig необходимо было прописать расширения для ts файлов, чтобы он правильно их “проходил мимо”

[options]
module.file_ext=.css
module.file_ext=.js
module.file_ext=.ts
module.file_ext=.tsx
module.file_ext=.json

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

Решения:
Гугло-яндекс поиск несомненно помогал. Вообще работа разработчика во многом связана с поиском информации, будь то https://developer.mozilla.org, или stackoverflow, или https://www.typescriptlang.org. Также большую помощь оказали наши «соседние» сервисы, к которым я не раз обращался на первом этапе. Просто так взять один конфиг и скопировать его к себе не получается.  В итоге изучив 3 наших сервиса я опробовал несколько подходов и настроек, поймал множество ошибок и спустя n-ое число подходов настроил все правильно. Как все же отличаются кодовые базы  даже тех сервисов, которые делаются в общем-то «рядом» и с похожими целями.

Тем временем коллега заканчивал работу над другими своими задачами и готовился присоединиться к процессу.

По итогу первой стадии у нас на выходе была ветка, в которой были сделаны базовые настройки сборки и проверки для ts/tsx файлов. Уже можно было начинать переносить компоненты.
Оглядываясь назад на проделанную работу, я понимаю, что переход от первого этапа ко второму мог бы быть несколько иным. Но об этом - чуть позже в блоке «Рефлексия». А сейчас…

Этап второй. Рефакторинг компонентов

Разработчиков уже двое. Необходимо работать над общей кодовой базой, границы распределения задач размыты, пересечения в коде неизбежны. Мы решаем, как будем разрабатывать совместно. Для этого вспоминаем, что наша задача - перенести компоненты на TS так, чтобы работающий на JS код остался функциональным. Связано это, прежде всего, с ограниченными сроками, мы не могли позволить себе перевести абсолютно все на TS, да этого и не требовалось. А требовалось выработать подход к файлам: менять ли существующие или создавать новые рядом, оставлять ли структуру компонентов, и как определить порядок изменений.

В итоге мы решили:

  1. Существующие js файлы утилит и функциональность redux-store мы не меняем, а копируем файлы и добавляем суффиксы к названию, чтобы в дальнейшем можно было легко менять импорты и видеть, что уже переведено, а что нет. Таким образом JS код использовал то же, что и раньше.

  2. Для компонентов и модулей, которые импортируются из наших JS библиотек создать тайпинги и положить к нам, вместо изменения в общих библиотеках.

  3. Структуру файлов внутри компонентов мы оставили неизменной, она была весьма удачной:
    Component

    • Component.container.tsx

    • Component.css

    • Component.type.ts

    • Component.tsx

    • Index.ts

  4. Функции ядра приложения и утилит для SSR меняем только в крайнем случае.

  5. Все разработки ведутся в одной feature-branch, в которую мы оба добавляли коммиты.

  6. Регулярный «живой» синк по тому, кто какой компонент/файл взял в работу.

Компоненты разделили и процесс пошел.

Нам повезло, я считаю, что у нас был flow.js, а не просто «голый» JS, потому что мы уже видели, какие типы используются. Нам оставалось только менять синтаксис и проверять, все ли учтено. Не раз попадались  места, где пропсы или были лишними и “прокидывались”, хотя этого не требовалось для компонента, или наоборот - тип был указан один, а передавались совершенно другие параметры, и по чистой случайности все работало из-за совпадения имен параметров.

После того, как мы договорились, как будем звать файлы, куда складывать типы, и как тот или иной синтаксис flow.js переводится на TS, нам предстояла весьма скрупулезная и монотонная работа. Сказать по правде, тот, кто делал сервис до нас, провел весьма качественную работу. Все компоненты и утилиты, стор, экшены и селекторы, все было подчинено определенным схемам, и порядок сохранялся. Это во многом облегчало нашу задачу. 

Процесс перевода компонента выглядел так:

  • Проверяем типы этого компонента и переписываем на новый синтаксис, меняем расширения файла на .ts.

  • В компоненте меняем расширение на .tsx, убираем все лишние аннотации // @flow, меняем экспорты на TS файлы.

  • Если где-то используются утилиты - идем и правим там. Эдакий обход дерева в глубину.

  • Проверяем «руками» что все нужные пропсы передаются, что ничего лишнего не прокидывается, чистим, наводим порядок.

  • Запускаем линтер и смотрим, на что еще жалуется система.

  • Делаем коммит.

  • Берем следующий компонент/файл и переходим к шагу «один».

Это была кропотливая работа, но предсказуемая. Где-то просто удавалось поменять синтаксис через cmd+shift+R замену в Intellij Idea сразу в пачке файлов. А где-то нужно было выполнять роль компилятора и внимательно следить, какие типы теперь используются и где.

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

Проблемы:
Начинают возникают конфликты в коде, которые неизбежны, когда несколько разработчиков ведут работу над общей частью кода.

Проблемы с flow.js и тем, как он дружит с TS. Хотя в документации flow и говорилось, что обрабатываются только файлы с аннотацией // @flow, но если JS файл в цепочке импортов где-то использовал внутри TS файл, то flow  смотрел и TS файл, и он совершенно не понимал конструкции вида ComponentProps[‘property’], не помогал даже .flowignore.

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

Решения:
Возникающие в коде конфликты были в первую очередь неудобством. Откат изменений, неудачного merge и rebase исправляли ситуацию. Регулярные обсуждения и синхронизации сделали исправление конфликтов нетрудным. Да и по большей части поделили области рефакторинга мы хорошо.

С flow.js, когда он ругался на .ts файлы, было сложней. Где-то мы, по возможности редактировали файлы по цепочке импортов, где-то мы уносили типы в глобальные тайпинги. В какой-то момент мы просто отключили проверку тайпингов flow.js, потому что уже не меняли файлы js.

Спустя спринт от начала нашей работы мне пришлось переключиться на другие продуктовые задачи и багофиксы. Мой коллега продолжил работу над переводом компонентов. А в какой-то момент и он переключился на продуктовую задачу - да так, что в ее основе уже лежал наш глобальный рефакторинг. Что из этого вышло - мы узнаем уже когда будем рефлексировать.

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

Этап третий. Тестирование

Как напоминает Мартин Фаулер в своей книге “Рефакторинг кода на Javascript” - перед рефакторингом убедись, что все покрыто тестами.

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

В нашей задаче мы планировали прогнать существующие регрессионные тесты и проверить руками наши страницы.

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

Благодаря большому количеству имеющихся автотестов мы уже на предварительном запуске сумели отловить несколько неприятных багов, которые были исправлены. Дальше было еще несколько запусков перед ручным тестированием.

Обычно для разработчика переход его задачи в столбец канбан-борда «In Testing» - это такой момент, когда можно немного расслабиться, ведь сделано все, что было нужно, и теперь процесс на тестировщике.

Но в этот раз расслабиться не получалось :) Тестирование было не быстрым. QA специалисты помимо нашей, ничуть не преувеличивая огромной задачи, работали еще над другими. На процесс тестирования понадобилось несколько дней.

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

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

Завершающий этап. День релиза

День релиза. Сказать по правде, это был крайне волнительный день. Мы, конечно же, не жали кнопку «Deploy» все вместе, но пальчики держали :)

Feature-branch обычным образом отправили в мастер, прогнали еще раз все автотесты, собрали версию и нажали «Deploy». А QA специалист еще куда дольше обычного следил за мониторами. После этого релиз считался успешным.

Ура, мы это сделали!

Отдельным поводом для нашей с коллегой гордости являлось то, что багов на проде из-за этого рефакторинга выявлено не было. Я считаю это большим успехом для всех нас, результатом кропотливой работы и разработчиков, и QA.


Рефлексия

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

- Что мы получили в итоге?
По итогу рефакторинга у нас были переведены на TS все UI компоненты,  весь стор, за исключением некоторых saga, часть функций ядра сервиса, там где это было необходимо, мы переводили код с ramda на нативный TS там,  где это было оправдано. Мы сформировали прочную базу для дальнейшей разработки уже только на TS.

- Что мы не делали, и что у нас осталось?
Мы не трогали библиотеки ядра, которые были сами по себе, не перевели на TS один большой компонент, содержащий сложную бизнес логику и использование xstate.js, и который планируется рефакторить отдельно. Мы не переводили наши же ui-библиотеки, которые импортировались как отдельные пакеты - это полностью отдельная активность. Мы практически не меняли jest тесты, .ts функции и утилиты подключились нормально.

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

Хотя у нас и был в распоряжении относительно большой кусок времени, и мы могли себе позволить перенести как можно больше компонентов сразу, все же я разбил бы задачу иначе. В другой раз я в начале бы сделал этап номер «один» с конфигами, сборщиками и линтерами,  отправил бы все это в production, и уже потом рефакторил компоненты дальше. Это позволило бы уже разрабатывать новые фичи сразу на TS и снизило бы эмоциональную нагрузку от продолжительной работы над задачей.

Что касается другого аспекта разработки, на мой взгляд разрабатывать вместе в одной ветке через коммиты, а не через merge request в общую feature-branch, было неудобно. Можно было бы избежать забывчивости, необходимости постоянно сверять, кто что сделал. Следующий рефакторинг, где мы работали вдвоем, мы делали через merge request, и проблем в плане синхронизации было меньше. Но все это исключительно вопрос договоренности между разработчиками, всегда есть несколько путей решения проблем :)

Выводы

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

На эту активность нужно явно выделять время и ресурс разработчика + тестировщиков, сделать “по ходу дела” не получится, это надо учитывать. Но оно того стоит. Дальнейшая разработка становится быстрей, понятней, приятней.

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

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

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

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

Удачи!

Комментарии (1)


  1. k12th
    10.06.2022 21:44
    +2

    Были ли какие-то плюсы или минусы от смены собственно типизации? Например, некоторые типы стали точнее или описываются короче, или наоборот? Удалось ли отловить какие-то баги за счет именно TS? Изменилось ли время сборки, и еcли да, то как?