Привет .
Меня зовут Михаил, более 10 лет разрабатываю интернет-проекты: от сайтов до систем автоматизации.

Миграция React-проекта на TypeScript

TL:DR

Всем привет. Не так давно я устроился в один банк frontend-разработчиком.

С первых дней я стал заниматься редизайном одного проекта: есть макеты, и 400 файлов с легаси. Легаси представляет собой React-приложение, побывавшее в руках десятка разработчиков и стартовавшее 3 года назад.

Предвидев объём работ, я предложил использовать TypeScript. К счастью, коллега после обсуждений согласился, и я начал настраивать проект и расставлять везде any.

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

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

1 Немного о проекте

В проекте примерно 10 сценариев со своими ветвлениями: вход по логину/телефону/QR/Push, регистрация, восстановление логина, смена пароля и пр. На каждый сценарий приходится в среднем по 3 API-запроса. Эти запросы отличаются не только разнообразием передаваемых данных, но и разнообразием ответов от бэка.

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

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

Оценив объём работ, стало понятно, что чистого JavaScript будет недостаточно, и нам была бы полезна статическая типизация, и TypeScript в частности.

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

Почему TypeScript?

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

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

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

Альтернативы вроде Flow или JSDoc рассматривались, но они потеряли актуальность.

2 Процесс

Изначально было около 400 js-файлов, из них 250 -- это компоненты.

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

Рекомендация №1 — От простого к сложному

Начинайте с простых модулей (то есть модулей, слабо связанных с остальными) и заканчивать более сложными.
В нашем случае самыми сложными в переносе были redux-экшены, так как они имели много зависимостей: redux-form, lodash, relelect, immutable и самописные утилиты. Весь этот 

Рекомендация №2 — Постепенная строгость

TypeScript имеет несколько основных настроек, влияющих на его строгость. Мы начинали с самой нестрогой конфигурации, перенося куски кода практически без изменений.
К примеру, есть флаг noImplicitAny. Если он включён, то TypeScript будет считать за ошибку место, где ему не известен тип переменной.

Рекомендация №3 — PropTypes не нужны

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

3 Профит

Описание API с помощью TypeScript
Описание API с помощью TypeScript

Самым значимым профитом стало более редкое обращение к спецификациям, которых может быть несколько для одного сценария.

В этом коде описан ответ от бэка при регистрации. Ответ содержит поля строковый token, булевый success и объект ошибки, описанный выше. Важно лишь в начале правильно описать типы. Таких описаний в проекте много. Это может показаться лишней тратой времени, но на самом деле разработчик так или иначе тратит время на изучение API, только здесь это нужно сделать всего один раз.

Использование дженерика AxiosResponse
Использование дженерика AxiosResponse

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

Автодополнение пропсов компонента
Автодополнение пропсов компонента

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

Автодополнение с помощью определения типов
Автодополнение с помощью определения типов
 

Упростилась работа со сторонними библиотеками. Даже еcли библиотека написана на чистом JavaScript, наверняка для неё есть описание типов, которое можно добавить в проект.

Автодополнение пропсов компонента
Автодополнение пропсов компонента

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

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

4 Подводные грабли

Определение типов сторонней библиотеки
Определение типов сторонней библиотеки

За всё время миграции сбило темп разве что отсутствие описания типов для библиотеки компонентов lib.ui. Это не камень в огород TypeScript или разработчиков библиотеки, а довольно штатная ситуация при использовании сторонних библиотек.

Тем не менее, пришлось самим создать описание компонентов и хранить его в проекте.

Подробнее о типизации сторонней UI-библиотеки React-компонентов можно прочесть в этой статье: TypeScript: описание типов сторонней UI-библиотеки React-компонентов, написанной на JavaScript.

5 Вывод

Миграция на TypeScript может быть простой.

Максимум, что мы использовали в TypeScript — это примитивные типы, интерфейсы и дженерики. Взамен мы значительно повысили надёжность кода и сэкономили время, концентрируясь на разработке, а не косвенных процессах вроде запоминания документации.

Осталось ещё много идей по улучшению качества кода с помощью TypeScript, но это совсем другая история.