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

Комментарии (28)


  1. justboris
    14.04.2017 16:22
    +1

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

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


    1. comerc
      14.04.2017 16:31

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


      1. Veikedo
        14.04.2017 16:34

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


        1. comerc
          14.04.2017 17:30

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

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


        1. Danakt
          14.04.2017 17:31
          +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)
                  // ...
              }
          }


          1. comerc
            14.04.2017 17:36

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

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


      1. justboris
        14.04.2017 20:14

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


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


        1. comerc
          14.04.2017 20:52

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

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


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

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


        1. comerc
          15.04.2017 22:19

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


          1. justboris
            16.04.2017 00:08

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


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


            1. comerc
              16.04.2017 00:16
              +1

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


  1. fakey0u
    14.04.2017 16:25

    flow-typed.

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

       flow-typed
        - npm
        - types
          - TableRowsType.js
          - AppType.js
    


    1. comerc
      15.04.2017 14:11

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


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


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


  1. gimntut
    14.04.2017 16:33

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

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




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


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

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


  1. comerc
    14.04.2017 17:18

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

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


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

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


    Настройка WebStorm:



  1. comerc
    14.04.2017 18:47

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


  1. arvitaly
    14.04.2017 23:20

    Какую версию 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


    1. comerc
      15.04.2017 11:42

      Какую версию TypeScript вы пробовали?

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


  1. unel
    15.04.2017 00:44
    +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);
    }


    1. comerc
      15.04.2017 11:40

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

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


      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 сыроват?


    1. comerc
      15.04.2017 19:11

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


    1. comerc
      16.04.2017 00:21

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


      type Props = {
        children?: typeof React.Element,
      }


    1. comerc
      16.04.2017 01:41

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

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


  1. kanstantsin
    16.04.2017 18:50

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


    1. comerc
      16.04.2017 19:42

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


      1. kanstantsin
        16.04.2017 21:05
        +1

        Я имею в виду:

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


        1. comerc
          16.04.2017 21:20

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


  1. HKA
    21.04.2017 17:26
    +1

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

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

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

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

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