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

  • Tutorial

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


case OPEN_POPUP: {
  const { config } = action;
  return {
    ...state,
    isOpen: true,
    config,
  };
}

То есть у вас есть state описанного типа, и вы говорите вернуть спред от state и новые поля; Flow не понимает, что мы спредим такие же поля, какие должны вернуть. Обещают это когда-нибудь поправить, Flow развивается очень быстро (пока есть обходной путь).


Но основная проблема 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-приложением (Илья Климов)


PS


Сейчас перевожу свой фан-проект на 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. А менять конфигурацию ESLint в CRA нельзя. И выход есть, снова применяю прекрасный react-app-rewired. Кстати, подключить tcomb он тоже помог, вся магия внутри config-overrides.js.


И вишенка на торте. Flow + абсолютные пути для импорта:


# .flowconfig
[options]
module.system.node.resolve_dirname=node_modules
module.system.node.resolve_dirname=src
Share post

Comments 29

    +1
    Но теперь ESLint ругается на нарушение правила no-use-before-define. А менять конфигурацию ESLint в CRA нельзя.

    А почему нельзя просто перенести определение Props в начало файла?

      0

      WebStorm сворачивает импорты. В результате, когда открываю файл, то первым делом вижу реализацию компонента. А типы по переходу в конец файла [CTRL]+[END]. Привычка.

        0

        Неправильная привычка у вас :)
        Props это как входные параметры функции -> идут первыми.
        Компонент это возвращаемый результат -> идёт последним

          0
          Неправильная привычка у вас

          Спорно. Помнится, ещё в Delphi можно было отдельно объявить тип для использования, и описывать его где-нибудь ниже по коду. Ностальгия. Я применяю деструктуризацию для props, т.е. имена аргументов видно сразу. А разглядывать их типы не особо интересно — информационный шум.

            +1

            Не обязательно. Обычно react-redux'овский mapStateToProps для компонента идёт вместе с экспортом, соответственно рядом с ними же стоить размещать типы параметров. Так что я считаю логичной такую структуру:


            // Компонент
            class SomeComponent extends Component {
                // ...
            }
            
            // Экспорт
            const mapStateToProps = state => ({
                prop: state.section.prop
            })
            
            SomeComponent.propTypes = {
                prop: React.PropTypes.string
            }
            
            export default connect(mapStateToProps)(SomeComponent)
            

            А вообще синтаксис flow позволяет такое:


            class SomeComponent extends Component {
                props: {
                    prop1: string,
                    prop2: number,
                }
            
                constructor(props) {
                    super(props)
                    // ...
                }
            }
              0
              А вообще синтаксис flow позволяет такое

              Это сделано для тех, кто привык использовать static propTypes, очевидно. Но засорять тельце компонента — как-то совсем, на мой вкус. И не добиться единообразия с функциональными компонентами.

            0

            Перекраивать create-react-app на кастомный конфиг только из-за привычки искать определения типов в конце?
            Мне кажется, это слишком радикальное решение.


            И во-вторых, no-use-before-define включен тоже не просто так, чтобы вам мешать. Код читается сверху вниз, если нам нужно посмотреть определение какой-то переменной, то его нужно искать выше по тексту. И определение типов тут не исключение.

              0
              Перекраивать create-react-app на кастомный конфиг только из-за...

              Не только, аппетит приходит во время еды: styled-jsx, module-resolver, tcomb.


              no-use-before-define включен тоже не просто так, чтобы вам мешать

              Я сразу предупредил, что "хочется странного" — это послание специально для вас.

                0

                Babel мне позволяет извращаться, а вы обратно запрещаете. Ну и кого мне слушаться? :)

                  0

                  Create-react-app — это не Babel, и конфигурации не имеет (react-app-rewired не в счет, потому что это неофициальное дополнение).


                  И я бы не стал отклоняться от его настроек. Сила create-react-app — в единообразии. Все проекты имеют одинаковый набор технологий и используют одинаковый стиль кода. Очень легко вникать в проект, даже если начинал его не я.

                    +1

                    Babel живет внутри CRA. И Babel из коробки это позволяет, конфигурировать его не надо.

                  +1

                  Признаю, это была сомнительная самодеятельность. WebStorm и VSCode позволяют сворачивать блоки. К сожалению WebStorm не запоминает состояние (Folding), в отличии от VSCode.

              0
              flow-typed.

              И добавить отдельную директорию для типов. Когда типы разростаются, то файлы получаются очень большие и не информативные. а вот intellisense так же парсит
                 flow-typed
                  - npm
                  - types
                    - TableRowsType.js
                    - AppType.js
              
                0

                Вот тут я сомневаюсь. Компоненты стремятся почковаться до атомарного состояния. Чем меньше — тем лучше. Это оправдано, хотя стучать по клаве утомляет. Отсюда и типы уменьшаются. А вот переключать контекст внимания между файлов — только раздражает. Я вкрячил CSS в компоненты посредством styled-jsx — ужасно доволен, например.


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


                https://github.com/comerc/yobr/blob/master/src/components/Post/Post.js

                0
                Это конспект части доклада

                Так вот почему текст так похож на стенограмму, с трудом осилил. А многое просто не понял.




                Ни когда не слышал про flow поэтому следующие вопросы такие нубские:


                И тут на помощь приходит восхитительнейший плагин… tcomb

                Это плагин от flow, от babel или от IDE?
                Какими IDE поддерживается flow?

                  0
                  Это плагин от flow, от babel или от IDE?

                  babel-plugin-tcomb — транспайлит код с применением tcomb.


                  Какими IDE поддерживается flow?

                  Nuclide точно поддерживает. Очевидно, что Visual Studio не отстает, но этого я не пробовал.


                  Настройка WebStorm:


                    0

                    Хотя собирался давно, но решился переехать на Flow после этой замечательной публикации: Зачем использовать статические типы в JavaScript? (Преимущества и недостатки).

                      0
                      Какую версию TypeScript вы пробовали?
                      Во второй гораздо сильнее вывод типов и strictNullChecks
                      function b(c: number | undefined) {
                          c.toExponential(); // Error: Object is possibly undefined.
                          if (typeof (c) !== "undefined") {
                              c.toExponential(); // No error
                          }
                      }
                      function d(c: number) {
                          // CODE
                       }
                      b(1);
                      d(undefined); // Error: Argument of type 'undefined' is not assignable to parameter of type 'number'.
                      
                      

                      Также очень вкусный keyof
                        0
                        Какую версию TypeScript вы пробовали?

                        Мопед не мой. Это вопрос к автору доклада.

                        +1

                        Flow классная штука, flow+tcomb — наверное, ещё круче.


                        Но всё-таки печалит тот факт, что некоторые вещи он не распознаёт корректно :-(
                        Вот примеры кода, на который ругается flow, но которые допускаются typescript-ом:


                        передача внутренних полей после проверки наличия объекта
                        /* @flow */
                        
                        type Person = {name: string, email: string};
                        type Bug = {id: string, assignee: ?Person};
                        
                        var newBug: Bug = {id:'bug1', assignee: null};
                        
                        if (newBug.assignee) {
                          console.log(newBug.assignee.name);
                          console.log(newBug.assignee.email);
                        }

                        асинхронный вызов после проверки наличия переменной
                        /* @flow */
                        
                        function syncOp(y: number): number {
                          return y;
                        }
                        
                        function asyncToSyc(x: ?number) {
                            if (x) {
                              setTimeout(() => syncOp(x), 100);
                            }
                        }

                        Согласен, что в некоторых случаях такие конструкции могут породить ошибки, но конкретно эти примеры содержат вполне безопасный код.
                        И это печально, что flow не смотря на "гораздо строгий и гораздо более мощный вывод типов, чем у TypeScript-а" не может разделять случаи, когда такое поведение безопасно, а когда — нет.


                        И вместо написания логики приложения, приходится выискивать способы, позволяющие flow мириться с твоим кодом =(

                          0
                          передача внутренних полей после проверки наличия объекта

                          Проверил, вот так работает без ругани:


                          type Person = {name: string, email: string};
                          type Bug = {id: string, assignee: ?Person};
                          
                          var newBug: Bug = {id:'bug1', assignee: null};
                          
                          if (newBug.assignee) {
                            var assignee = newBug.assignee;
                            console.log(assignee.name);
                            console.log(assignee.email);
                          }

                          асинхронный вызов после проверки наличия переменной

                          Это тоже легко обойти исправлением типа. Наверно тут можно сделать скидку, что Flow сыроват?

                            0

                            Ещё один косяк нашёл: https://github.com/facebook/flow/issues/3742

                              0

                              Победил property children not found in props of React element


                              type Props = {
                                children?: typeof React.Element,
                              }
                                0
                                И вместо написания логики приложения, приходится выискивать способы, позволяющие flow мириться с твоим кодом =(

                                Закончил сегодня переезд с PropTypes на Flow. На очереди редюсеры. И по ощущениям пляски вокруг Flow оправданы (я тоже очень не люблю, когда мне навязывают что-то сверх необходимого, похерил по этой причине redux-form, например). Выявлено несколько ошибок, которых не замечал раньше. Только WebStorm тормозит опять, зараза. Как с eslint-ом было, пока не отрубил все лишнее.

                                0
                                А можно ли заменить PropTypes на Flow для контекста? Насколько я представляю, пока от PropTypes отказаться нельзя, т.к. они используются для typehinting контекста.
                                  0

                                  Покажите проблемный участок кода. Будем работать. :)

                                    +1
                                    Я имею в виду:

                                    class Button extends React.Component {
                                      render() {
                                        return (
                                          <button style={{background: this.context.color}}>
                                            {this.props.children}
                                          </button>
                                        );
                                      }
                                    }
                                    
                                    Button.contextTypes = {
                                      color: PropTypes.string // Как получить контекст без PropTypes?
                                    };
                                    
                                      0

                                      Есть babel-plugin-flow-react-proptypes, но автор отказывается допиливать contextTypes, хотя призывает PR — тынц.

                                  +1
                                  Мы честно попытались, грубо говоря, переименовать проект из JS в TS — оно не заработало. Оно не компилируется, потому что некоторые вещи, с точки зрения TypeScript-а являются некорректными. Это не означает, что TypeScript — плохой язык, но продвигаться на идее надмножества, и подводить меня так, TypeScript — я не ожидал.

                                  Вы неверно понимаете эту идею. TS является надмножеством JS, т.к. полностью поддерживает синтаксис и семантику последнего. Но поскольку TS призван обеспечить статическую типизацию, т.е. является более строгим с т.з. type safety, разумеется, простое изменение расширения файла не работает. Иначе какой смысл переходить на TS?

                                  Тем не менее, разработчики языка учли проблемы миграции и поэтому ввели в конфиг всякие noImplicitAny, allowUnreachableCode и прочие ослабляющие параметры. Проще говоря, включите их все — и простого переименования будет достаточно.

                                  И тут на помощь приходит восхитительнейший плагин, который для меня является киллер-фичей, почему сейчас я выберу Flow, а не TypeScript

                                  Поверхностный гуглеж предолжил мне, как минимум, это: Runtypes, Reflec-TS. Наконец, есть генерация в JSON Schema, на котором построено много runtime-валидаторов.

                                  Only users with full accounts can post comments. Log in, please.