Привет! Меня зовут Владислав Коротун, я — ведущий фронтенд-разработчик в одной из продуктовых команд hh.ru. Сегодня я расскажу о том, как мы затащили TypeScript в наши проекты.
Мир Дикого Запада
Многие из нас пришли в профессию, когда фронтенд готовился на PHP, а затем оживлялся с помощью jQuery. Это был настоящий Дикий Запад: каждый писал, как хотел, не было линтеров, препроцессоров, шаблонизаторов, не было даже вебпака! И ничего — справлялись. На адептов типизированных языков мы смотрели как на тоталитарную секту: ну очевидно же, что ребята попусту тратят время, описывая разные типы и интерфейсы. Однако с тех пор прошло много времени, фронтенд изменился, а вслед за ним изменились и мы.
В те далёкие годы, когда jQuery правил балом, я очень любил Javascript за его простоту и невероятную гибкость, которая была возможна благодаря его прототипной природе.
Пришла в голову гениальная идея нового контрола или анимации — бегом за комп, создаешь новый HTML-файл, добавляешь jQuery с CDN, создаешь блок <script>, и внутри у тебя полная свобода самовыражения. Никакой предварительной работы по продумыванию интерфейсов сущностей и взаимодействия между ними. Нужно что-то сделать с блоком — селектишь и делаешь. Нужна строка — завел переменную. Что, это больше не строка? Окей, теперь это число, а позже мы присвоим ему функцию. Звучит жутковато, правда?
Новый дивный мир
Но смутные времена подошли к концу. Где-то в начале 10-х годов начали появляться фреймворки для создания Single-Page-Application: Ember, AngularJS, React. Каждый фреймворк вносил свои абстракции, которые мы дополняли особенностями наших проектов. Кодовая база стремительно росла, фронтенд стал разделяться на разные слои, появился собственный роутинг, механизмы управления состояниями. Начали набирать популярность бандлеры для клиентского кода и репозитории пакетов.
В ответ на увеличение сложности проектов стали появляться и линтеры с космическим количеством правил на любой вкус — разработчики старались сделать код безопаснее и понятнее. Но как ни покрывай код всевозможными проверками, то и дело в прод просачиваются различные “undefined is not a function”, “cannot get property of undefined” и прочие штуки, способные заставить усомниться в выборе профессии даже самого матерого фронтендера.
Погружение в контекст занимало всё больше времени. Даже при необходимости сделать какую-то минорную правку в компоненте приходилось детально изучать, что и откуда в него прилетает, какие там данные, может ли там внезапно оказаться null или undefined. Конечно, у нас есть eslint. Но его полномочия — всё, когда приходится проанализировать что-то динамическое, особенно тип, который в итоге получается в конкретном месте после многочисленных преобразований. Кроме того, у нас есть и React.PropTypes. Но я думаю, многие видели проптайпы, описанные как PropTypes.object, что сводило их полезность к нулю.
И что же с этим всем делать?
Deus ex TypeScript
Проблемы, которые я описал, имеют одинаковую природу: JavaScript — это язык с динамической типизацией. Когда ты меняешь тип переменной в одном месте, забыв поменять способ обращения к ней в другом, об ошибке ты узнаешь только в рантайме.
Это не очень понравилось фронтендерам из Microsoft. И в 2012 году они создали собственный диалект JavaScript под названием TypeScript. В своих первых версиях он не блистал изяществом, гибкостью и глубиной анализа кода, но позволял явно описать типы переменных, структуру объектов и сигнатуру функций. Этого оказалось достаточно чтобы сообщество среагировало, и TypeScript начал набирать популярность далеко за пределами Microsoft.
TypeScript в hh.ru
В 2018 году, когда я только пришел в hh.ru, вопрос во фронтенд-чате о том, не хотим ли мы TypeScript, собирал значительно больше дизлайков, чем лайков. Я сам в то время ставил жирный минус и бежал в комментарии описывать всю свою нелюбовь к “бесполезной” работе по описанию типов и корпорации, подарившей нам своё детище. Но всё поменялось, когда в формате техдолга мне досталась задачка по доработкам нашего внутреннего инструмента для разбора инцидентов.
Инструмент этот был написан на Node.js/React. Одно дело, когда ты регулярно что-то делаешь в проекте и совершенно другое, когда залезаешь туда раз в год. Когда у меня N-раз подряд структура данных отдаваемая с бэка не сошлась с той, что я ожидал увидеть на фронте, я задумался: раз у меня в обоих местах JS, не переписать ли мне это все на TS (благо, инструмент небольшой), заодно пошарив типы между фронтом и бэком?
Сказано — сделано. Пару выходных я потратил на то, чтобы на практических задачах познакомиться с TS и получить от него сколько-нибудь ощутимую пользу. В итоге, когда все js-файлы были переименованы в ts, все красные подчеркивания из редактора кода пропали, бизнес-задачу я сделал буквально за полчаса.
Я уже предвидел, насколько удобно будет вернуться сюда через те же полгода, чтобы добавить очередную удобняшку и не тратить время на изучение: кто, что и куда прокидывает. Все типы данных описаны, если попытаешься круглое вставить в квадратное — редактор подсветит.
Этой радостной новостью я спешил поделиться с коллегами на очередном митапе. Нет, после этого митапа мы всей толпой не ломанулись дружно изучать TS, но у нас собралась инициативная группа неравнодушных заинтересованных, с которыми мы начали строить планы по дальнейшему внедрению TypeScript.
Первый сервис на TS
В предыдущих статьях (здесь и здесь) я уже упоминал наш веб-чатик. Мне повезло оказаться у руля этого сервиса, поэтому я принял решение с самого начала писать его на TypeScript. Много утилитарного кода перекочевало из нашего основного монолита. Какие-то вещи удавалось переписывать сразу на TS. Другие, посложнее, я описывал с помощью d.ts-файлов — это возможность описать экспортируемые элементы из модуля, не трогая в нем самом ни строчки.
В процессе работы всё больше наших common-решений переводилось полностью на TS. Какие-то модули было достаточно просто грамотно описать и кое-где применить дженерики. Но были и такие, которые без полной переработки перевести на TS не удавалось.
Чемпион TypeScript
У нас в компании действует система чемпионства. Можно выбрать какую-то сферу для улучшений или новую технологию, презентовать программу по “втаскиванию” или “допиливанию” с обозначением конечных целей. Затем собирается группа экспертов, способных оценить суть программы, проводится митап, обсуждаем, голосуем и, если всех всё устраивает, автор программы становится чемпионом выбранного направления.
Так я и поступил с TypeScript. Цель программы — добиться того, чтобы весь новый код можно было писать на TS. Программа включала в себя:
Переработку ключевых механик монолита.
Типизирование Redux: хотя бы основных редьюсеров и core-компонентов, без которых не добавить новую страницу.
Типизирование всех компонентов из нашей UI-библиотеки.
Добавление TS в наш стартап для новых фронтенд-сервисов и переписывание его boilerplate-кода.
Кроме того, в рамках программы и с помощью инициативной группы, мы провели серию обучающих митапов для коллег, чтобы познакомить их с особенностями типизированного мира. Любые знания лучше закреплять на практике, поэтому я подготовил огромное количество задач с меткой ts-beginners и подробным описанием, что и как требуется сделать. Задачи по большей части включали в себя перевод UI-компонентов из нашей библиотеки на TypeScript.
Уже через полгода после старта программы, с некоторыми оговорками, новые страницы и компоненты можно было писать на TypeScript. А через год программа была полностью завершена. С тех пор всё новое обязательно пишется на TS.
В сухом остатке
Сам по себе TypeScript не избавит вас от ошибок. Да что там, сам по себе он даже не может ограничить использование any. Но всё меняется, когда eslint получает доступ к информации о типах. Существует целый блок правил из раздела recommended — requires type checking. И с этими правилами вы сможете получить куда более глубокий анализ кода и приблизиться к принципу “if it lints – it works”.
Когнитивная нагрузка на программиста значительно снижается: нет необходимости проводить ресерч вокруг затрагиваемого компонента, вся нужная информация содержится в типах. А когда приходится залезть в какой-нибудь нетипизированный компонент, первое что чешутся руки сделать — это переписать его на TS.
Да, у нас ещё остались кусочки на JS в монолите. Но этот код крайне редко трогают, и, вероятнее всего, он будет переведен на TS, когда потребуется в нем что-то изменить. Весь новый код и все наши микрофронтенды мы целиком пишем на TS.
Одна из продуктовых команд сделала заход на генерацию типов из Java. Таким образом можно напрямую связать интерфейсы, которыми мы оперируем во фронтенде, с теми данными, которые возвращает бэкенд. Ребята делают успехи, но для глобального использования решение пока не готово.
Ещё один крайне приятный бонус: рефакторинги типизированного кода проводить одно удовольствие. Достаточно поменять то, что тебе нужно поменять, и прогнать tsc –noEmit. Компилятор сам пройдется по всему проекту и выведет все правки, которые осталось внести, чтобы проект снова заработал.
Искать разработчиков на чистом JS в 2022 году становится всё труднее. Многие даже не рассматривают вакансии в компании, в которых не применяется TypeScript. И их можно понять — сложно из условно безопасного мира TS возвращаться в когнитивный ад JavaScript.
Но есть и минусы:
Проект стал собираться чуть дольше.
Валидный и правильный код, который работал в JS, при переводе на TS зачастую приходится переписывать.
Задачи с применением дженериков сложно решать новичкам, приходится помогать, подсказывать, проводить митапы.
Есть неоднозначные вещи в самом TS. Это касается типизации встроенных методов массивов и объектов, вроде Object.keys / Object.values и Array.includes. Но TS унаследовал у JS его гибкость, поэтому сигнатуры даже настолько фундаментальных вещей легко расширяются и их можно адаптировать для использования в своем проекте.
На этом моя история подходит к концу. Если вы до сих пор не попробовали TS или считаете, что ваш проект слишком большой для этого, надеюсь, наш опыт поможет вам взглянуть на ситуацию под другим углом и развеет ваши страхи.