В 2016-м статически типизированный JavaScript оказался крайне востребованным. Теми или иными средствами, позволявшими устранить недостатки динамической природы JS, воспользовались многие компании. Нас тоже привлекла перспектива задействовать огромный потенциал статической типизации в своих разработках.
Выбирая инструменты, мы предварительно остановились на TypeScript и Flow. Хотя проекты эти и различаются, направлены они на решение одной и той же задачи. А именно, позволяя контролировать типы данных, они обеспечивают более совершенную, в сравнении с чистым JS, организацию кода, улучшают возможности рефакторинга, позволяют программистам работать быстрее и увереннее.
Coherent Labs и TypeScript
Мы начали присматриваться к типизированному JS в прошлом году, когда проект, над которым я сейчас работаю, визуальный редактор пользовательских интерфейсов для HTML-игр, написанный преимущественно на JavaScript, дорос до более чем 100 тысяч строк кода. Редактор оснащён массой возможностей, как результат, его кодовую базу стало сложнее поддерживать и подвергать рефакторингу. Именно поэтому мы решили провести некоторые исследования и найти решение для встающей перед нами проблемы.
Через некоторое время мы пришли к двум вариантам — TypeScript и Flow. Оба обещали дать нам статические типы для улучшения контроля над кодом, но одного этого было недостаточно. Мы хотели использовать всю мощь ES6 и будущих версий языка. Flow, по сути, это статический анализатор кода, что означает, что нам пришлось бы использовать транспилятор для всего нашего ES6-кода, но применяя TypeScript, который является надстройкой над JavaScript, мы получили бы и статические типы, и поддержку большинства последних возможностей ECMAScript.
Учитывая вышесказанное, было несложно принять окончательное решение. Мы остановились на TypeScript.
О популярности статически типизированного JavaScript
Давайте немного отвлечёмся и поговорим о том, что такое статическая типизация, и как она нашла своё место в динамическом мире JavaScript.
Основная разница между статически типизированными и динамически типизированными языками заключается в том, как они производят проверку типов данных. Первые делают это во время компиляции, а вторые — во время исполнения программы. Другими словами, статически типизированные языки требуют от программиста объявлять типы данных перед их использованием, в то время как в динамически типизированных языках это не нужно.
JavaScript, являясь динамически типизированным языком, позволяет использовать различные типы данных в разнообразных операциях. Для разработчиков, которые пришли в JS со статически типизированных языков, вроде Java или C++, это, в большинстве случаев, выглядит совершенно нелогично и даже странно. Подобное приводит к серьёзным неприятностям в проектах большого масштаба. Например, программисту может понадобиться потратить многие часы только на то, чтобы выяснить, отчего вдруг некая переменная оказалась NaN.
TypeScript и Flow были созданы для решения подобных проблем. Поддержкой Flow, который приобрёл популярность в последние два года и ориентирован лишь на проверку типов, занимается Facebook. TypeScript же, к которому приложила руку Microsoft, с другой стороны, является надстройкой над JavaScript. Это означает, что он не только поддерживает проверку типов, но и позволит работать с будущими возможностями ES6 и ES7. И у того, и у другого, имеются компиляторы, преобразующие код в чистый JavaScript, который можно запустить в любом браузере.
Вот некоторые преимущества использования типизированного JavaScript:
- Он позволяет улучшить читаемость кода.
- Он способствует раннему выявлению ошибок.
- Он даёт возможности для более надёжного рефакторинга.
- Он улучшает поддержку языка на уровне IDE.
Принимая решение о возможной разработке проектов с использованием типизированного JavaScript, задайте себе следующие вопросы:
- Значителен ли проект для вашего бизнеса?
- Состоит ли проект из множества сложных частей?
- Есть ли вероятность того, что вы соберётесь подвергнуть проект рефакторингу для того, чтобы он лучше соответствовал вашим потребностям?
- Есть ли у вас большая команда, в которой разработчикам необходимо до минимума сократить время на изучение кодовой базы и поддержку собственных знаний о ней в актуальном состоянии?
Если большинство ответов на эти вопросы положительны, значит вам стоит рассмотреть возможность переноса проекта на TypeScript или Flow.
Вечная битва титанов: TypeScript и Flow
Как уже было сказано, и Flow, и TypeScript дают JS-разработчику очень важную, и, по моему мнению, весьма необходимую возможность — систему типов.
TypeScript появился в 2012-м, при содействии Microsoft, когда Андерс Хейлсберг выпустил его первый релиз. Кроме прочего, он поддерживает необязательные аннотации типов и возможности ES6 и ES7. У него имеется компилятор, который обрабатывает аннотации и генерирует код на обычном JavaScript.
Flow — это статический анализатор кода, которым занимается Facebook. Он спроектирован так, чтобы быстро находить ошибки в JavaScript-приложениях. Это — не компилятор, а анализатор. Он может работать вообще без каких-либо аннотаций типов, отлично показывая себя в задаче вывода типов переменных.
Взглянем на простой пример. Имеется следующая функция, которая, принимая имя, возвращает приветствие:
function greet = function(name) {
return `Hello, ${name}!`;
}
И с использованием TypeScript, и с применением Flow, эта функция будет выглядеть примерно так:
function greet(name: string): string {
return `Hello, ${name}!`;
}
«String» после параметра функции — это способ задания типа в TypeScript и Flow. Типы могут быть как простыми, так и составными. Это означает, что если мы попытаемся использовать аргумент, тип которого отличается от заданного, будет выдано сообщение об ошибке. Рассмотрим пример использования функции.
const message = greet(43);
Функцию мы вызвали, передав ей в качестве аргумента число. При компиляции кода TypeScript выведет сообщение о том, что произошла ошибка:
Argument of type 'number' is not assignable to parameter of type 'string.'
Вылавливание подобных ошибок во время компиляции может весьма положительно сказаться на производительности труда и даже на настроении разработчика. TypeScript, кроме того, поддерживает современные возможности JS, такие, как стрелочные функции, импорт и экспорт, генераторы, механизмы async/await, классы, и так далее. В итоге оказывается, что TypeScript позволяет сформировать отличную экосистему разработки приложений на JavaScript.
Итак, теперь позвольте мне рассказать о том, как мы перевели проект, содержащий более 200 тысяч строк кода, на TypeScript.
Шаг первый — выбор технологии
Наш проект стремительно развивался, со временем объём кодовой базы увеличивался практически экспоненциально. В те времена росла популярность TypeScript и статически типизированного JavaScript. Во всём этом мы увидели возможность, которую нам не следовало упускать. После некоторых изысканий и пары совещаний мы остановились на двух возможных походах:
- Использовать Flow и Babel для транспиляции ES6.
- Использовать TypeScript и его компилятор для ES6/ES7 компиляции.
Мы выбрали именно TypeScript по нескольким причинам:
- В те времена у TypeScript было более обширное сообщество, по нему было больше материалов.
- Нам не хотелось усложнять процесс сборки приложения, который и так был довольно тяжёлым. Добавление ещё двух шагов при использовании Flow (компиляция Flow и транспиляция Babel) замедлили бы работу ещё сильнее. TypeScript же позволял обойтись одним дополнительным этапом сборки, что показалось нам вполне приемлемым.
- TypeScript лучше поддерживают интегрированные среды разработки. Вся наша команда пользуется WebStorm, которая обеспечивает отличную поддержку TypeScript.
- Поддержка ES5. Ядро нашей системы построено на WebKit и JavaScriptCore, с учётом определённых ограничений. Некоторые из стандартных возможностей ES5 не поддерживаются. В результате, возможности TypeScript сыграли ведущую роль при принятии окончательного решения.
После шести месяцев работы, я могу откровенно сказать, что выбор TypeScript был верным решением. Не всё в процессе переноса проекта шло гладко, но ни с чем слишком сложным мы не столкнулись.
Шаг второй — начало
После долгого обсуждения процесса перехода, в котором участвовала вся команда, мы решили начать с маленького модуля и переписать его. Самое начало — это замена расширений файлов с .js на .ts.
Далее была настройка сборочного инструмента (Grunt) с использованием grunt-ts, и, дня через полтора, первый TypeScript модуль был готов. Главной трудностью тогда была работа с глобальными и внешними модулями. TypeScript имеет жёсткие правила, касающиеся типизации. Как результат, компилятор TypeScript не распознаёт вспомогательные библиотеки, вроде jQuery и Kendo UI, и их методы. Это сразу же дало более 200 ошибок компиляции, указывающих на то, что TypeScript не может найти переменную типа «$». Решение было довольно простым — мы воспользовались инструкциями declare
:
declare var $;
declare var kendo;
...
Ключевое слово declare
используется для создания так называемых окружающих объявлений. Они служат для предоставления TypeScript информации о других библиотеках, о типах данных, источником которых является не TS-файл. В той ситуации это оказалось вполне приемлемым решением.
На данном этапе мы успешно перевели на TypeScript первый модуль и были готовы уйти в работу с головой, переводя весь проект на TypeScript.
Шаг третий — переход
Следующим шагом был перевод всех остальных модулей на TypeScript. Некоторые из них были сильно взаимосвязаны и надо было придумать способ быстро сделать их работоспособными. После некоторых дополнительных исследований и пары часов мозговых штурмов, мы пришли к сравнительно простой стратегии.
Для начала мы переименовали все файлы, дав им расширение .ts, и тут же столкнулись со шквалом ошибок компиляции. Затем добавили необходимые объявления TypeScript и назначили некоторым переменным тип данных Any
. Это позволяет записывать в переменную данные различных типов. Таким способом объявляли переменные, которые могут хранить данные разных типов, скажем, строки, числа, логические значения.
После двухдневной борьбы с типами и объявлениями, мы вышли на первую полную компиляцию TypeScript. Однако, работы оставалось ещё очень много. Надо было улучшить проверку типов, добавить соответствующие объявления для внешних библиотек и подвергнуть ES5-код переработке под стандарты ES6.
Файл объявлений описывает устройство библиотеки. Применяя их (ещё их называют файлами .d.ts), можно избежать неправильного использования библиотек и получить удобства вроде автозавершения команд в редакторе кода. При работе с TypeScript 1.8 для того, чтобы добавить новый файл объявлений, нужно установить отдельные npm-пакеты глобально, найти файлы объявлений для конкретной библиотеки и сохранить все файлы описаний библиотек в папке «typings». Честно говоря, решение это было не самое элегантное. В TypeScript 2.0 имеется новый способ управления объявлениями библиотек — использование реестра npm. Делается это всё теперь парой команд. Сначала — установка:
npm install --save @types/jquery
Потом — импорт там, где это нужно:
import * as $ from 'jquery'
В результате — нет больше скучной возни с файлами и папками.
Итак, с внешними библиотеками мы разобрались. Следующим шагом было изменение всех объявлений с типом Any
на подходящие типы данных. Эта задача заняла несколько дней. Мы решили не торопиться и поэтапно перерабатывать код. В частности, выполняли рефакторинг под ES6 и изменение типов. После пары недель мы инкрементально перевели на ES6 80% кода, снабдив его осмысленными аннотациями типов. На данном этапе можно было переходить к следующей стадии работы — тестированию.
Тестирование и TypeScript
Наша среда разработки была близка к идеалу и мы были готовы к следующему большому этапу работы над проектом — сквозному (E2E) тестированию. Мы решили использовать средства JavaScript. Наш арсенал — это Mocha, Chai, Selenium и WebDriver Selenium. Мы изолировали код тестирования — фреймворк Selenium, тесты и все зависимости, предоставив им отдельный tsconfig.json.
Присутствие файла tsconfig.json в директории указывает на то, что директория является корнем проекта TypeScript. Этот файл задаёт корневые файлы и параметры компилятора.
Нам пришлось создать один такой файл для самого проекта и подвергнуть рефакторингу задачу Grunt для корректного использования разных конфигурационных файлов. Теперь мы были готовы начать работу с фреймворком Selenium.
Структура фреймворка состоит из семи основных и пяти вспомогательных модулей. Вот основные модули:
- Обработчик драйвера, который запускает и останавливает Selenium и управляет тайм-аутами.
- Модуль селектора, содержащий все элементы, которые используются в тестах.
- Главный файл действий, который ответственен за организацию работы с различными частями приложения и выполнение взаимодействий с ним, вроде щелчков мышью, выделения элементов, ввода данных.
- Модуль для экспорта и удаления ресурсов.
- Модуль ведения журналов.
- Модуль создания копий экрана, который делает скриншот после каждого проваленного теста.
- Модуль для имитации API бэкенда.
Вспомогательные модули разделены по категориям тестов. Так, у нас был модуль исполнения JavaScript, модуль панели свойств, модуль панели временной шкалы, и так далее. Все они обладают методами для выполнения специфических задач.
Кроме того, мы добавили средство создания отчётов — mochawesome, которое выводило полный отчёт по всем тестам после их завершения.
Благодаря TypeScript в проекте удалось задействовать возможности async/await, в результате, мы пришли к более чистому и лучше организованному коду.
Планы на будущее
На самом деле, то, о чём я рассказал, это только начало. Переход на TypeScript — лишь капля в море. Сделать нам предстоит ещё очень много. Вот некоторые из наших планов:
- Произвести рефакторинг всей архитектуры проекта.
- Добавить новые функции, такие, как 3D-выделение и ограничительные рамки, визуальные указатели привязки данных элементов и многое другое.
- Улучшить общее время исполнения тестов. Сейчас для выполнения 440 тестов требуется порядка 30 минут.
Кроме того, мы экспериментируем с библиотеками, поддерживающими работу с виртуальным DOM, наподобие React и InfernoJS. И та и другая отлично поддерживают TypeScript, проект может значительно выиграть от прироста производительности, который даёт работа с виртуальным DOM.
Мы внимательно следим за развитием TypeScript. Последние релизы представляют новые возможности, вроде отображения типов, операторов spread и rest для работы с объектами, поддержки асинхронных функций при компиляции в ES3 и ES5. В будущих релизах ожидается поддержка генераторов для ES3/ES5, улучшение средств обобщённого программирования, асинхронные итераторы.
Итоги
Решение использовать TypeScript в нашем проекте оказалось правильным, принесло много хорошего. В частности, это — повышение производительности труда, улучшение управляемости кода, поддержка ES6. Изначально я с недоверием относился к TypeScript, но после работы с ним могу сказать, что он — это то, чего мне очень долго не хватало.
Хотя я очень рекомендую испытать TypeScript, поэкспериментировать, вы, задумываясь о переводе собственного проекта на TypeScript, должны принять во внимание все детали. Надо учесть время, необходимое для переработки кодовой базы, особенности проекта, возможную потребность в рефакторинге, и, самое главное — мнение команды разработчиков о TypeScript.
В итоге хочу сказать, что TypeScript, безусловно, можно считать значительным явлением, воздействие которого на мир JavaScript сделает его лучше.
Уважаемые разработчики! А как вы относитесь к средствам статической типизации в JavaScript? Если бы вы решили переработать большой JS-проект, что бы вы выбрали?
Автор: RUVDS.com