- PVSM.RU - https://www.pvsm.ru -
Рано или поздно, все приходят к выводу, что нам нужна строгая типизация. Почему? Потому что проект разрастается, обрастает if-ами; функциональное программирование — всё функция — неправда, мне только что консоль сказала "undefined is not a function". Вот эти проблемы появляются всё чаще-чаще, становится сложнее отслеживать, возникает вопрос — давайте строго типизировать, хотя бы на этапе написания кода будет подсказывать.
Знаете рекламу: TypeScript — это надмножество JavaScript-а. Маркетинговый BS. Мы честно попытались, грубо говоря, переименовать проект из JS в TS — оно не заработало. Оно не компилируется, потому что некоторые вещи, с точки зрения TypeScript-а являются некорректными. Это не означает, что TypeScript — плохой язык, но продвигаться на идее надмножества, и подводить меня так, TypeScript — я не ожидал.
Как только вы вычеркиваете TypeScript, остаётся ровно одна альтернатива — Flow. Что я могу сказать про Flow? Flow мегакрутой тем, что заставит вас выучить систему типов OCaml, хотите вы того, или нет. Flow написан на OCaml. У него гораздо строже и гораздо мощнее вывод типов, чем у TypeScript-а. Вы можете переписывать проект на Flow частично. Количество бонусов, которые вам приносит Flow, сложно описать. Но, как всегда, есть парочка "но".
Хорошие. У нас начинают появляться вот такие штуки — это кусок редюсера:
type BuyTicketActionType = {|
type: BuyTicketActionNameType,
|}
type BuyTicketFailActionType = {|
type: BuyTicketFailActionNameType,
error: Error,
|}
Пайпы "|" внутри фигурных скобок означают строгий тип — только эти поля и ничего более. На вход редюсера обязаны приходить только такие-то экшены:
type ActionsType =
| BuyTicketActionType
| BuyTicketFailActionType
;
Flow это красиво верифицирует. Казалось бы всё превосходно, но нет. Flow работает только с типами. Приходится писать извращения:
type BuyTicketActionNameType = 'jambler/popups/buyBonusTicket/BUY_TICKET';
const BUY_TICKET: BuyTicketActionNameType
= 'jambler/popups/buyBonusTicket/BUY_TICKET';
Поскольку вы не можете объявить константу, и сказать, что такой-то тип является значением этой константы; проблема курицы и яйца, константа — это уже код, который должен быть типизирован, а типы не должны взаимодействовать с кодом. Поэтому приходится говорить, что тип BuyTicketActionNameType — это какая-то строка, и дальше, что константа BUY_TICKET имеет такой же тип, исключительно ради того, чтобы проконтролировать, что строка совпадает. Слегка извращение.
Что ещё. Эти строгие типы очень крутые, очень удобно позволяют выявлять опечатки и прочее; вот только они не понимают spread-оператор [1]:
case OPEN_POPUP: {
const { config } = action;
return {
...state,
isOpen: true,
config,
};
}
То есть у вас есть state описанного типа, и вы говорите вернуть спред от state и новые поля; Flow не понимает, что мы спредим такие же поля, какие должны вернуть. Обещают это когда-нибудь поправить, Flow развивается очень быстро (пока есть обходной путь [2]).
Но основная проблема Flow, что типы, которые вы пишите, напоминают предвыборную программу депутатов Верховной Рады Украины. То есть вы предполагаете, что некоторые типы будут туда приходить, а на самом деле туда приходит не совсем то, что вы ожидаете. К примеру, вы ожидаете, что в компонент всегда будет приходить пользователь, а иногда туда приходит null — всё, вы не поставили знак вопроса, Flow это никак не отловит. То есть полезность Flow начинает падать, как только вы начинаете его накручивать на существующий проект, где у вас в голове вроде как есть понимание, что происходит, но на самом деле это не всегда происходит так, как вы задумали.
Ещё есть backend-программисты, которые любят менять форматы данных, и не уведомлять вас об этом. Мы начинаем писать JSON-схемы, чтобы валидировать данные на входе и на выходе, чтобы в случае чего говорить, что проблемы на вашей стороне.
Но как только вы начинаете писать JSON-схемы, получаете два источника типизации: JSON-схемы и Flow. Поддерживать их в консистентном состоянии — такой же миф, как о поддержке актуальности JSDoc-ов. Говорят, где-то есть программисты, которые поддерживают JSDoc-и в абсолютно актуальном состоянии, но я их не встречал.
И тут на помощь приходит восхитительнейший плагин, который для меня является киллер-фичей, почему сейчас я выберу Flow, а не TypeScript почти на любом проекте. Это tcomb (babel-plugin-tcomb). Что он делает? Он берёт Flow-типы и реализует проверки в рантайме. То есть когда вы описываете систему типов, ваши функции в development-режиме будут автоматически проверять входные данные и выходные данные на соответствие типов. Не важно, откуда эти данные вы получили: в результате парсинга JSON, и так далее, и так далее.
Превосходная штука, как только вы подключаете в проект, следующие два дня понимаете, что все Flow-типы, которые у вас написаны, на самом деле не так. Он говорит: "слушай, ты тут написал, что приходит Event — это на самом деле SyntheticEvent реактовский". Ты же не подумал, что в React-е все Event-ы — это SyntheticEvent. Или там: "слушай, у тебя пришёл null". И каждый раз падает-падает-падает. Справедливости ради, падает только в development-режиме. Тот странный момент, когда в production всё продолжает работать, а разрабатывать невозможно. Но очень сильно помогает.
У нас есть функции и типы, tcomb просто транспилирует в assert-ы; но самое коварное, он выполняет на все типизированные объекты Object.freeze() — это означает, что вы не можете не просто добавить к объекту поле, вы даже в массив пушнуть ничего не можете. Вы любите иммутабельность? Ну так вот, пожалуйста. Вместе с tcomb вы будете писать иммутабельный код, хотите вы того, или нет.
Это конспект части доклада Хайп против реальности: год жизни с изоморфным React-приложением (Илья Климов) [3]
PS
Сейчас перевожу свой фан-проект [4] на Flow. Хочется странного, чтобы код компонента был выше, чем объявление типа для props.
До:
import React from 'react'
import PropTypes from 'prop-types'
const MyComponent = ({ id, name }) => {
//...
}
MyComponent.propTypes = {
id: PropTypes.number,
name: PropTypes.string,
}
После:
// @flow
import React from 'react'
const MyComponent = ({ id, name }: Props) => {
//...
}
type Props = {
id: number,
name: string,
}
Но теперь ESLint ругается на нарушение правила no-use-before-define [5]. А менять конфигурацию ESLint в CRA нельзя. И выход есть, снова применяю прекрасный react-app-rewired [6]. Кстати, подключить tcomb он тоже помог, вся магия внутри config-overrides.js [7].
И вишенка на торте. Flow + абсолютные пути для импорта:
# .flowconfig
[options]
module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=src
Автор: comerc
Источник [8]
Сайт-источник PVSM.RU: https://www.pvsm.ru
Путь до страницы источника: https://www.pvsm.ru/javascript/252768
Ссылки в тексте:
[1] spread-оператор: https://developer.mozilla.org/ru/docs/Web/JavaScript/Reference/Operators/Spread_operator
[2] обходной путь: https://github.com/facebook/flow/issues/2405#issuecomment-274073091
[3] Хайп против реальности: год жизни с изоморфным React-приложением (Илья Климов): https://www.youtube.com/watch?v=niRATPKKF40&t=1104
[4] фан-проект: https://github.com/comerc/yobr
[5] no-use-before-define: http://eslint.org/docs/rules/no-use-before-define
[6] react-app-rewired: https://github.com/timarney/react-app-rewired
[7] config-overrides.js: https://github.com/comerc/yobr/blob/master/config-overrides.js
[8] Источник: https://habrahabr.ru/post/326538/
Нажмите здесь для печати.