Журнал / польза

Типизируй это. Фронтенд-разработчик Яндекса рассказывает о TypeScript

Что такое статическая типизация и как TypeScript помогает писать красивый и логичный код.

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

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

Плюсы статической типизации

Я занимаюсь фронтенд-разработкой во внутреннем облаке Яндекса, на котором работают наши сервисы. Изначально весь фронтенд облака был написан на JavaScript: нам нужно было быстро запустить проект и иметь возможность быстро добавлять в него новую функциональность. По сути это был стартап, но с ростом проекта поддерживать код стало сложно.

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

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

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

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

В TypeScript все типы определены и задокументированы, что делает код чистым и легко читаемым даже для того, кто только подключился к проекту.

Исторически было несколько попыток типизировать веб. Сначала появились языки с собственным синтаксисом, например Elm, Reason и Dart. Но для их освоения нужно было учить новый синтаксис, поэтому победил второй подход: языки, которые расширяют синтаксис JavaScript, — Flow и TypeScript. У Flow была более сильная система, но в итоге популярным стал TypeScript.

Почему TypeScript?

При создании TypeScript был сделан упор на максимально безболезненный переход с JavaScript. Благодаря этому он превзошёл по популярности другие языки со статической типизацией. Поскольку TypeScript не привносит самостоятельный синтаксис, а дополняет синтаксис JavaScript, порог входа и время на адаптацию минимальны: код выглядит практически так же, а для освоения базовой функциональности достаточно пары недель. Кроме того, TypeScript обратно совместим с JavaScript, компилируется в него и поддерживает популярные JS-библиотеки. Именно поэтому он выиграл гонку языков с Flow, где синтаксис более непривычный и есть не очень интуитивные вещи.

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

За время существования TypeScript стал стандартом де-факто во фронтенд-разработке: практически все популярные библиотеки работают на TypeScript или имеют декларации типов.

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

В TypeScript есть подсказки, быстрый поиск и переход к нужной функции, а также массовое переименование типов, поэтому на нём удобно работать в IDE. VSCode, WebStorm, Sublime Text и даже Vim поддерживают этот язык.

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

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

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

Тьюринг-полнота и другие фишки TypeScript

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

TypeScript поддерживает использование перечислений (enum). Они позволяют определять набор именованных констант и упрощают документирование намерений и сценариев использования. При этом они могут быть как численными, так и текстовыми. Подробно об использовании перечислений в TypeScript можно почитать здесь.

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

Для случаев, когда вам важно зависеть от названия типа, то есть реализовать номинативную типизацию, можно воспользоваться приёмом «брендирование типов».

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

Автоматизация интеграционных тестов — это не самый стандартный случай использования TypeScript, но она может сберечь вам немало времени. Элементам сайта (формам, кнопкам и всему остальному) присваиваются текстовые ключи, под них генерируются типы в TypeScript. В итоге в тестах мы получаем подсказки для всех элементов: пишем button и получаем в подсказках редактора выпадающий список всех кнопок.

Но всё же главная фишка TypeScript — это дженерики и всё, что с ними связано.

Зачем нужны дженерики?

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

  ​Пример вывода типов для стандартного метода массивов map.
  ​Пример вывода типов для стандартного метода массивов map.

Есть ли минусы?

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

Один из минусов заключается в том, что TypeScript ничего не знает о рантайме. Это критично, если вы обрабатываете много данных из бэкенда: при неверно расставленных типах данные будут обрабатываться некорректно, а ошибку будет невозможно отследить. Но всё решается договоренностью с командой бэкенда о том, какие данные в каком виде отдаются.

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

Синтаксис дженериков запутанный и многословный. Сами по себе дженерики могут быть непривычны и контринтуитивны, особенно при переходе с JavaScript. Но всё становится ещё запутаннее, если на дженериках пытаются запрограммировать какую-то многоэтажную логику. Как я уже говорил, это Тьюринг-полная система, на которой можно написать что угодно. При этом синтаксис достаточно бедный: есть только перебор по ключам и условный оператор. В итоге объект с глубокой вложенностью превращается в 150 строк кода, что практически нечитаемо.

Отсутствует управление вариантностью: в TypeScript нельзя явно установить ограничение на тип уже или шире заданного в синтаксисе.

Система типов не такая богатая, как в других языках. В TypeScript дженерики первого порядка: мы можем параметризовать тип только простым типом — строкой, числом или объектом. В некоторых языках, например в Haskell, дженерики можно параметризовать другими дженериками, что даёт дополнительные возможности для функционального программирования. Хорошая новость, что в промышленной разработке вам обычно хватает полиморфизма первого порядка.

По большому счёту, все перечисленные минусы TypeScript — это неудобства, с которыми сталкивается разработчик при переходе с менее строгого языка программирования. К ним достаточно легко привыкнуть, и они с лихвой окупаются стройностью кода, которую вы получите, когда перенесёте проект с JavaScript.

Сложности, с которыми мы столкнулись при переезде на TypeScript

Когда мы перенесли проект облака на TypeScript, мы смогли существенно упростить и облегчить код, избавившись от дополнительных проверок в рантайме. Однако некоторые вещи оказалось не так просто перенести с JavaScript.

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

Другой сценарий, в котором статическая типизация не очень хорошо справляется, — это различные кейсы с символами. В JavaScript есть специальный тип symbol, который используется как идентификатор для свойств объектов. До недавнего времени в TypeScript его нельзя было использовать в качестве индекса. Приходилось явно импортировать отдельные символы и делать для каждого тип. Но поскольку использование символов не очень распространено, все подобные случаи можно локализовать через any.

Кроме того, мы столкнулись с небольшими сложностями при подключении модулей и интеграций, но нам удалось с ними разобраться:

Отсутствие автообработки расширений файлов для ESM-модулей. TypeScript-компилятор ничего не знает про расширения файлов. При сборке UI это не мешает, так как обычно уже подключён бандлер (Webpack, Vite и др.), но для запуска тестов и отдельных скриптов бандлер тащить не хочется. Мы используем ts-node, который позволяет запускать ts-файлы, скрывая процесс компиляции.

Сложность соединения с API и бэкендом. Языки бэкенда имеют свои форматы описания API, например protobuf. Чтобы синхронизировать бэкенд с фронтендом, мы написали решение, которое перегоняет все protobuf-структуры в TypeScript-структуры. И, хотя у нас нет общего кода, мы переиспользуем устройство API на уровне типов.

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

Давайте подытожим, в каких случаях переезд на TypeScript необходим, а когда достаточно возможностей, которые нам даёт JavaScript.

Когда использовать TypeScript:

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

— Если вы работаете с данными и в проекте есть сложная бизнес-логика.

— Если вы работаете в экосистеме, где уже есть хорошая поддержка TypeScript.

— В крупных компаниях даже маленькие проекты предпочтительно писать на TypeScript, потому что в проект важно заложить возможности для усложнения и роста уже на старте.

— Если вы пишете библиотеку.

Когда достаточно JavaScript:

— Если вы работаете с лендингами и простыми сайтами: визитками, блогами на основе markdown и т. п.

— Если вам нужно сделать прототип и быстро протестировать какую-то гипотезу.

Что почитать и посмотреть для подготовки

Руководство по TypeScript — то, с чего стоит начать.

Five Things About TypeScript. Коротко о TypeScript словами его создателя, технического партнёра Microsoft Андерса Хейлсберга.

Мой доклад о типизации для Школы разработки интерфейсов. Познакомимся с общими концепциями и особенностями устройства TypeScript: функциями, структурами, массивами и дженериками. Посмотрим на типы как на множества, на вложенность типов, выясним, что такое типы any и unknown. В практической части перенесём проект с JavaScript.

— Если вы хотите использовать TypeScript в Node.js, посмотрите это видео.

Библиотека io-ts. Библиотека позволяет отлавливать ошибки статических типов и преобразовывать типы в нужный формат при обработке запросов сервером.

Продвинутые дженерики в TypeScript. Ещё одна моя лекция для Школы разработки интерфейсов: о дженериках и работе с вложенными параметрами.

— Если вы работаете с библиотекой React, обратите внимание на это руководство по стилю.