- PVSM.RU - https://www.pvsm.ru -

Flow + tcomb = типизированный JavaScript

Рано или поздно, все приходят к выводу, что нам нужна строгая типизация. Почему? Потому что проект разрастается, обрастает 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/