Эта статья изначально была опубликована 7 февраля 2020 г. на Dev.to.

С началом нового десятилетия команда инженеров Fellow.app решила взять на себя новогоднее решение и сделать то, что нас сдерживало: мы решили преобразовать наше веб-приложение CoffeeScript в TypeScript. Это история о том, как мы, как команда, сумели преодолеть проблемы на нашем пути, и как завершение этой масштабной миграции улучшило наш продукт.

С самого начала мы высоко ценили возможность быстрой итерации функций на основе отзывов, которые мы получаем от команд, которые используют Fellow, и CoffeeScript был частью того, что обеспечило такую ​​скорость. CoffeeScript - это язык, который компилируется в JavaScript и предназначен для уменьшения занимаемого места кода и улучшения читабельности, а также добавления новых функций, которые позже будет внедряться в JavaScript (например, чистый способ определения классов). Поскольку все разработчики в команде уже знали Python, CoffeeScript был естественным выбором из-за сходства между ними (например, отсутствие фигурных скобок и использование пробелов для логического разделения), что упростило переключение контекста с одного языка на другой. Меньше времени на мысленное переключение между двумя разными синтаксисами и необходимость писать меньше фигурных скобок и круглых скобок сделали нас более эффективными при построении функций, и мы смогли не отставать от нашей ценности быстрой итерации.

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

// The following is valid CoffeeScript. Can you easily understand the operation?
square = (x) -> x * x
add = (x, y=1) -> x + y
console.log(add square add 2, add 1) // 17

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

13 декабря 2019 года, ровно через 10 лет после выхода CoffeeScript, мы сели и начали планировать наш полный план перехода с языка после того, как летом медленно преобразовали некоторые файлы здесь и там. Мы хотели начать использовать язык с сильным сообществом, эффективными инструментами отладки, поддержкой IDE, который позволил бы нам избежать как можно большего числа ошибок. TypeScript соответствует всем требованиям: аннотации типов позволят нам уменьшить количество ошибок во время выполнения, выступая в качестве мини-документации для нашего кода, доступные интеграции инструментов и редакторов безграничны, и это то, что мы были рады писать код вместо того, чтобы бояться . Наш переходный план? Сядьте вместе в команде на послеобеденное время для The Decaffeination и преобразуйте десятки тысяч строк CoffeeScript в TypeScript, прежде чем отправиться в отпуск на каникулы, и, в конечном итоге, выпустить преобразование в рабочую среду за один раз. И мы сделали это.

Мы позволили плану удаления CoffeeScript настояться в нашей голове в течение нескольких дней, а затем вылили наши идеи в общие заметки о встрече, чтобы все могли их увидеть. . Мы составили список файлов, которые необходимо преобразовать, и добавили их в качестве элементов действий в той же заметке, назначив каждый из них члену команды, чтобы мы были более или менее равны в предстоящей задаче. Мы решили создать ветку git, посвященную задаче, decaffeinate, и все отправили туда наш код. Через неделю, 20 декабря, мы приступили к работе.

Но даже несмотря на то, что мы в конечном итоге добились успеха, задача не обошлась без проблем! Сидя все вместе на диванах и в удобных креслах, попивая чай (что угодно, кроме кофе), мы могли действительно хорошо работать вместе: любые проблемы, с которыми мы сталкивались, кричали, и все помогали, и каждый успешно преобразованный файл отмечался. Некоторые из самых сложных моментов, с которыми нам пришлось столкнуться, были результатом нашего технического стека (мы используем React / Relay), создания ошибок, которые не дали результатов в StackOverflow, и новых ошибок, которые было трудно отследить.

Мы многому научились, взявшись за это преобразование, и обнаружили, что в Интернете есть ограниченные ресурсы о нашем конкретном стеке, поэтому мы хотели бы внести свой вклад в сообщество и поделиться своими проблемами. Если вы хотите преобразовать проект в TypeScript, я настоятельно рекомендую прочитать Подробное сообщение в блоге Доминика Кунделя о том, как начать.

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

Строчная интерполяция

У нас есть много строк в кодовой базе, куда мы вставляем переменные, что в CoffeeScript выглядит так:

"You have  #{num_1on1s} upcoming 1-on-1s today."

Но в TypeScript та же самая строковая интерполяция будет выглядеть так:

`You have ${num_1on1s} upcoming 1-on-1s today.`

Поскольку TypeScript обнаруживает интерполяцию строк с помощью обратных тиков, а не кавычек, нам пришлось быть особенно осторожными, чтобы проверить наш преобразованный код на наличие строк, которые необходимо преобразовать вручную, потому что TypeScript буквально выводит «У вас # {num_1on1s} предстоящий 1- сегодня он-1 ». если мы не обновили формат до обратных тиков и знаков доллара.

Неправильные типы для сторонних библиотек

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

Ввод компонентов более высокого порядка

В React Компонент высшего порядка - это функция, которая принимает компонент и возвращает новый компонент, цель состоит в том, чтобы повторно использовать код, который изменяет компоненты аналогичным образом (например, HOC может принять компонент и вернуть загрузка счетчика вместо компонента, если какой-либо вызов API еще не завершен, что является шаблоном, который вы можете использовать для многих различных областей).
Компоненты высшего порядка, как известно, сложно вводить из-за диапазона типов компонентов которые могут быть переданы им, и все различные свойства, которые есть у этих компонентов. В нашей кодовой базе у нас есть один особенно большой HOC withNoteStream, который мы используем для управления большим количеством данных и логикой рендеринга для частей проекта, которые имеют потоки заметок. А заметок у нас много! Личные заметки, заметки встреч, заметки один на один, заметки о целях,… все эти разделы имеют свои собственные компоненты, которые используют withNoteStream, и каждый имеет свой собственный уникальный набор свойств, которые необходимо передать. Мы еще не нашли отличного способа обойти эту проблему без преобразования типов в общий компонент, и это постоянное обсуждение в сообществе React / TypeScript.

Типы GraphQL по умолчанию не определены

Мы используем GraphQL для передачи данных между нашим интерфейсом React и серверной частью Django, поэтому, когда мы начали преобразование в TypeScript и обнаружили, что типы GraphQL по умолчанию не определены, нам пришлось либо написать весь наш код TypeScript, чтобы проверить наличие наличие данных перед их использованием (_8 _...), или нам нужно было найти другое решение.
Мы используем Graphene, чтобы предоставить наши модели Django для GraphQL, и мы частично решили проблему undefined, обернув поля списка в graphene.NonNull () и установив required=True в полях узла.
Однако это решение не работало для DjangoConnectionFields, который представляет собой тип поля, который используется для создания разбитого на страницы списка связанных объектов. Поля с этим типом все еще рассматривались как возможно неопределенные в TypeScript, даже если бы мы с уверенностью знали, что это не так. Чтобы исправить эту проблему, мы создали следующий класс для наследования, который установил бы для всех узлов значение required=True и всех ребер graphene.NonNull(), чтобы мы могли быть спокойны при написании кода внешнего интерфейса:

class NonNullConnection(graphene.relay.Connection, abstract=True):  # type: ignore
   @classmethod
   def __init_subclass_with_meta__(cls, node, edge_name=None, **kwargs):
       if not hasattr(cls, 'Edge'):
           _node = node
           class EdgeBase(graphene.ObjectType):
               class Meta:
                   name = edge_name or f'{node.__name__}Edge'
               cursor = graphene.String(required=True)
               node = graphene.Field(_node, required=True)
           cls.Edge = EdgeBase
       if not hasattr(cls, 'edges'):
           cls.edges = graphene.List(graphene.NonNull(cls.Edge), required=True)
       super(NonNullConnection, cls).__init_subclass_with_meta__(node=_node, **kwargs)

Ввод всего, что переданы неявные реквизиты

Написание {...props} в компоненте - хороший и простой способ передать реквизиты в компонент, который вы расширили, без необходимости явно указывать, что это за реквизиты (возможно, потому, что компонент, который вы пишете, не заботится о том, что они есть, и вы не хотите поддерживать их в нескольких местах). Однако в TypeScript это вызывало у нас проблемы, потому что для каждого компонента, для которого мы это делали, нам нужно было найти все перестановки передаваемых свойств и правильно их ввести. Несмотря на то, что это был длительный процесс, в итоге он того стоил, потому что он также обеспечил некоторый рефакторинг! Рефакторинг, как правило, является рискованной идеей при преобразовании кода из-за возможности внесения ошибок как в рефакторинг, так и в перевод (почти не указывается, что вызвало проблему), но мы смогли удалить реквизиты, которые, как мы обнаружили, мы больше не использовали на all, что также уменьшило количество типов, которые нам нужно было разрешить.

Несколько недель спустя, когда мы закончили преобразование последних нескольких строк и протестировали каждый последний кусочек продукта, мы объединили наш запрос на извлечение без кофеина в нашу основную ветвь и с нетерпением ждали, когда количество ошибок уменьшится, а читаемость кода повысится 🎉

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

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

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