- PVSM.RU - https://www.pvsm.ru -
JavaScript – это один из языков с динамической типизацией. Такие языки удобны для быстрой разработки приложений, но когда несколько команд берутся за разработку одного большого проекта, лучше с самого начала выбрать один из инструментов для проверки типов.
Можно начать разрабатывать код на TypeScript или включить в проект Flow. TypeScript – это компилируемая версия JavaScript, разработанная компанией Microsoft. Flow, в отличие от TypeScript, это не язык, а инструмент, который позволяет анализировать код и проверять типы. В сети можно найти множество статей и видео об этих подходах, а также руководство по тому, как начать использовать типизацию. В этой статье мы бы хотели рассказать, почему нам не подошел Flow, и как мы начали переходить на Typescript.
В 2016 году мы начали разрабатывать веб-клиент на базе React/Redux для нашей ECM системы. Для проверки типизации был выбран Flow по следующим причинам:
Но проект рос, количество команд-разработки увеличилось, и проявился ряд проблем при использовании Flow:
foo() {
if (this.activeFormContainer != null) {
// to do something
if (this.activeFormContainer != null) // only for Flow
this.activeFormContainer.style.minWidth = '100px';
}
}
}
У некоторых разработчиков появилась идея попробовать перейти на TypeScript. Для того чтобы проверить идею перехода и убедить руководство, решили попробовать прототип.
На прототипах мы хотели проверить две идеи:
Для первой идеи нужна была утилита, которая сконвертировала бы все файлы проекта. В сети нашли одну из таких. Судя по описанию, она смогла бы перевести большую часть, но часть изменений пришлось бы править самим, либо дописать саму утилиту. Нам удалось сконвертировать тестовый проект с небольшим количеством файлов. Реальный же проект скомпилировать так и не удалось, пришлось бы править слишком большое количество файлов. Решили не продолжать в этом направлении, так как:
Хотя мы и отказались от этого варианта, на нем мы получили полезный опыт. Стал ясен примерный объем работ, который нужно проделать для перевода каждого файла. Вот как примерно выглядит перевод простого React-компонента.
Как видно, изменений не так много. В основном, они заключаются в следующем:
Реализация по второй идее позволила бы продолжить разработку, но уже на TypeScript, и в фоновом режиме потихоньку переводить существующую кодовую базу. Это давало ряд преимуществ:
Но было не до конца ясно, можно ли настроить проект для работы с двумя видами типизации параллельно. Поиск в интернете ни к чему конкретному не привел, поэтому стали разбираться сами. В теории, анализатор Flow проверяет только файлы с расширением js/jsx и содержащие комментарий:
//@flow
или
/* @flow */
Для компилятора TypeScript файлы должны иметь расширение ts/tsx. Из чего следует, что оба подхода к типизации должны работать одновременно и не мешать друг другу. На основании этого мы настроили окружение проекта. Используя опыт от первого прототипа, перевели пару файлов. Скомпилировали проект, запустили клиент — всё заработало как раньше!
И вот в один прекрасный день — день планирования спринта, у нашей команды в бэклоге появляется User Story “Начать переход на TypeScript”, с следующим перечнем работ:
Первым делом нужно научить webpack обрабатывать файлы с расширением ts/tsx. Для этого добавили правило в секцию rules конфигурационного файла. Изначально использовался ts-loader:
// webpack.config.js
const rules = [
...
{
test: /.(ts|tsx)?$/,
loader: 'ts-loader',
options: {
transpileOnly: true
}
}
];
Чтобы ускорить сборку, отключили проверку типов: transpileOnly: true
, т.к. IDE и так указывает на ошибки во время написания кода.
Но когда приступили к переводу наших Redux-экшенов, стало ясно, что для их работы необходим плагин babel-plugin-transform-class-display-name [1]. Этот плагин добавляет всем классам статическое свойство displayName. Экшены после перевода стали обрабатываться только ts-loader, а это не позволило применить к ним плагины babel. В результате, мы отказались от ts-loader и расширили существующее правило для js/jsx, добавив babel [2]/preset-typescript:
// webpack.config.js
const rules = [
{
test: /.(ts|tsx|js|jsx)?$/,
exclude: /node_modules|lib/,
loader: 'babel-loader?cacheDirectory=true'
},
...
];
// .babelrc.js
const presets = [
[
"@babel/preset-env",
{
"modules": !isTest ? false : 'commonjs',
"useBuiltIns": false
}
],
"@babel/typescript",
"@babel/preset-react",
];
Для правильной работы компилятора TypeScript нужно добавить конфигурационный файл tsconfig.json, он был взят из документации.
Написанный с использованием Flow код дополнительно проверялся с помощью eslint. Для TypeScript есть его аналог — tslint. Изначально хотелось все правила из eslint перенести в tslint. Была попытка синхронизации правил через плагин tslint-eslint-rules, но большинство правил не поддерживается. Также есть возможность использовать eslint для проверки ts-файлов с помощью typescript-eslint-parser. Но, к сожалению, к eslint-у можно подключить только один парсер. Если использовать только ts-parser для всех видов файлов, появляется много непонятных ошибок как в js-файлах, так и в ts. В результате, использовали рекомендуемый набор правил, расширенный под наши требования:
// tslint.json
"extends": ["tslint:recommended", "tslint-react"]
Теперь все готово, и можно приступать к переводу файлов. Для начала решили перевести небольшой React-компонент, который используется по всему проекту. Выбор пал на компонент “Кнопка”.
В процессе перевода столкнулись с проблемой: не все сторонние библиотеки имеют типизацию TypeScript, например, bem-cn-lite. На ресурсе TypeSearch [3] от Microsoft библиотеку типов для нее найти не удалось. почти для всех необходимых библиотек мы нашли и подключили ts-библиотеки типов. Одним из решений было подключение через require:
const b = require(‘bem-cn-lite’);
Но при этом проблема с отсутствием типов не решилась. Поэтому мы сгенерировали «заглушку» для типов самостоятельно, воспользовавшись утилитой dts-gen [4]:
dts-gen -m bem-cn-lite
Утилита сгенерировала файл с расширением *.d.ts. Файл поместили в папку @types и настроили tsconfig.json:
// tsconfig.json
"typeRoots": [
"./@types",
"./node_modules/@types"
]
Далее, по аналогии с прототипом, мы перевели компонент. Скомпилировали проект, запустили клиент — всё заработало! Но сломались тесты.
Для тестирования приложения мы используем Storybook и Mocha.
Storybook используется для визуального регрессионного тестирования (статья [5]). Как и сам проект, он собирается с помощью webpack и имеет свой конфигурационный файл. Поэтому для работы с ts/tsx-файлами его нужно было сконфигурировать по аналогии с конфигурацией самого проекта.
Пока мы использовали ts-loader для сборки проекта, у нас перестали запускаться тесты Mocha. Для решения этой проблемы в тестовое окружение необходимо добавить ts-node:
// mocha.opts
--require @babel/polyfill
--require @babel/register
--require test/index.js
--require tsconfig-paths/register
--require ts-node/register/transpile-only
--recursive
--reporter mochawesome
--reporter-options reportDir=../../bin/TestResults,reportName=js-test-results,inlineAssets=true
--exit
Но после перехода на Babel от этого можно было избавиться.
В процессе перевода мы столкнулись с большим количеством проблем различной степени сложности. В основном они были связаны с отсутствием у нас опыта работы с TypeScript. Вот несколько из них:
При использовании компонентов/функций из разных типов файлов появилась необходимость указывать расширение файла:
import { foo } from ‘./utils.ts’
Избавиться от этого позволяет добавление допустимых расширений в конфигурационные файлы webpack и eslint:
// webpack.config.js
resolve: {
…
extensions: [ '.tsx', '.ts', '.js' ]
}
// .eslintrc.js
"import/resolver": {
"node": {
"extensions": [
".js",
".jsx",
".ts",
".tsx",
".json"
]
}
}
Из всех типов файлов больше всего проблем вызвал перевод компонентов высшего порядка (Higher-Order Component, HOC). Это функция, которая на вход принимает компонент и возвращает новый компонент. Применяется в основном для повторного использования логики, например, это может быть функция, добавляющая возможность выделять элементы:
const MyComponentWithSeletedItem = withSelectedItem(MyComponent);
Или наиболее известная connect, из библиотеки Redux. Типизация таких функций не тривиальная и требует подключения дополнительной библиотеки для работы с типами. Подробно описывать процесс перевода не буду, так как в сети можно найти много руководств на эту тему. Если вкратце, то проблема заключается в том, что такая функция – абстрактная: на вход может принять любой компонент, с любым набором свойств. Это может быть компонент «Кнопка» со свойствами title и onClick или компонент «Картинка» со свойствами alt и imgUrl. Набор этих свойств нам заранее не известен, известны лишь те свойства, которые добавляет сама функция. Для того, чтобы компилятор TypeScript не ругался при использовании компонентов, полученных с помощью таких функций, нужно «вырезать» свойства, которые добавляет функция из возвращаемого типа.
Для этого нужно:
interface IWithSelectItem {
selectedItem: number;
handleSelectedItemChange: (id: number) => void;
}
React.ComponentType<Diff<TPropsComponent, IWithSelectItem>>
Для работы с исходниками, например, выполнение code review, мы используем Team Foundation Server. При переводе файлов мы столкнулись с одной неприятной особенностью. В пул реквестах вместо одного измененного файла появляется два:
Такое поведение наблюдается, если изменений в файле много (similarity < 50%), например для небольших по объему файлов. Для решения этой проблемы пробовали использовать:
Но, к сожалению, оба подхода нам так и не помогли.
Использовать Flow или же TypeScript — решает каждый для себя сам, оба подхода имеют свои плюсы и минусы. Мы для себя выбрали TypeScript. И на своем опыте убедились: если вы выбрали один из подходов и вдруг осознали, даже спустя три года, что он вам не подходит, то всегда можно его поменять. А для более гладкого перехода можно настроить проект, как и мы, на параллельную работу.
На момент написания статьи мы еще не полностью перешли на TypeScript, но основную часть — «ядро» проекта – мы уже переписали. В кодовой базе можно найти примеры перевода всех видов файлов, начиная от простого react-компонента и заканчивая компонентами высшего порядка. Также было проведено обучение среди всех команд разработчиков, и теперь каждая команда в рамках своей задачи на тех долг переводит часть проекта.
Мы планируем завершить переход до конца года, перевести тесты и storybook, и, возможно даже написать несколько своих tslint-правил.
По личным ощущениям могу сказать, что разработка стала занимать меньше времени, проверка типов делается на лету, при этом не нагружая систему, а сообщения об ошибках лично для меня стали более понятными.
Автор: teager
Источник [7]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/325761
Ссылки в тексте:
[1] babel-plugin-transform-class-display-name: https://www.npmjs.com/package/babel-plugin-transform-class-display-name
[2] babel: https://habr.com/ru/users/babel/
[3] TypeSearch: https://microsoft.github.io/TypeSearch/http://
[4] dts-gen: https://www.npmjs.com/package/dts-gen-sandbox
[5] статья: https://habr.com/ru/post/454464/
[6] utility-types: https://github.com/piotrwitek/utility-types
[7] Источник: https://habr.com/ru/post/462055/?utm_campaign=462055&utm_source=habrahabr&utm_medium=rss
Нажмите здесь для печати.