Flow + tcomb = JS typed

  • Tutorial

Sooner or later, everyone concludes that we need strong typing. Why? Because the project is growing, overgrown with ifs; functional programming - the whole function is not true, the console just told me "undefined is not a function". These problems appear more and more often, it becomes more difficult to track, the question arises - let's strictly typify, at least at the stage of writing the code it will prompt.


You know the ad: TypeScript is a superset of JavaScript. Marketing BS. We honestly tried, roughly speaking, to rename the project from JS to TS - it did not work. It does not compile, because some things are incorrect from the point of view of TypeScript. This does not mean that TypeScript is a bad language, but to advance on the idea of ​​a superset, and let me down like that, TypeScript - I did not expect.


Once you cross out TypeScript, exactly one alternative remains - Flow. What can I say about Flow? Flow is mega-cool in that it makes you learn the OCaml type system, whether you want it or not. Flow is written in OCaml. It has much stricter and much more powerful type inference than TypeScript. You can partially rewrite the project on Flow. The amount of bonuses that Flow brings you is hard to describe. But, as always, there is a couple of “buts.”


Good ones. Such things start to appear here - this is a piece of a reducer:


type BuyTicketActionType = {|
  type: BuyTicketActionNameType,
|}
type BuyTicketFailActionType = {|
  type: BuyTicketFailActionNameType,
  error: Error,
|}

Pipes "|" inside braces mean strict type - only these fields and nothing more. Only such actions are required to enter the input of the reducer:


type ActionsType =
  | BuyTicketActionType
  | BuyTicketFailActionType
;

Flow verifies this beautifully. Everything seemed to be excellent, but no. Flow only works with types. I have to write perversions:


type BuyTicketActionNameType = 'jambler/popups/buyBonusTicket/BUY_TICKET';
const BUY_TICKET: BuyTicketActionNameType
  = 'jambler/popups/buyBonusTicket/BUY_TICKET';

Because you cannot declare a constant, and say that such-and-such is the value of this constant; the problem of chicken and eggs, a constant is already a code that should be typed, and types should not interact with the code. Therefore, we have to say that the BuyTicketActionNameType type is some kind of string, and further that the BUY_TICKET constant is of the same type, solely to ensure that the string matches. Slightly perverse.


What else. These strict types are very cool, very convenient to detect typos and more; But they don’t understand the spread operator :


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

That is, you have state of the described type, and you say to return the spread from state and new fields; Flow does not understand that we will spread the same fields that we must return. They promise to fix it someday, Flow is developing very fast (as long as there is a workaround ).


But the main problem of Flow is that the types you write resemble the election program of deputies of the Verkhovna Rada of Ukraine. That is, you assume that some types will come there, but in fact, not exactly what you expect comes there. For example, you expect that the user will always come to the component, and sometimes null comes there - that's all, you did not put a question mark, Flow will not catch it. That is, the usefulness of Flow begins to decline as soon as you start to wind it up on an existing project, where you kind of have an understanding of what is happening, but in reality this does not always happen the way you intended.


There are also backend programmers who like to change data formats and not notify you about this. We are starting to write JSON schemes in order to validate the input and output data, in which case we can say that the problems are on your side.


But as soon as you start writing JSON schemes, you get two typing sources: JSON schemes and Flow. Maintaining them in a consistent state is the same myth as supporting the relevance of JSDocs. They say that somewhere there are programmers who support JSDoc in an absolutely current state, but I have not met them.


And here an amazing plugin comes to the rescue, which for me is a killer feature, why now I will choose Flow, and not TypeScript on almost any project. This is tcomb (babel-plugin-tcomb). What is he doing? It takes flow types and implements runtime checks. That is, when you describe a type system, your functions in development mode will automatically check the input data and output data for type matching. It doesn’t matter where you got this data from: as a result of parsing JSON, and so on and so forth.


An excellent thing, as soon as you connect to the project, for the next two days you realize that all the Flow types that you have written are actually not. He says: "Listen, you wrote here that Event is coming - it’s actually SyntheticEvent Reaktovsky." You didn’t think that in React all Events are SyntheticEvent. Or there: "listen, you got null." And every time it falls, falls, falls. In fairness, it crashes only in development mode. That strange moment when everything continues to work in production, but it’s impossible to develop. But it helps a lot.


We have functions and types, tcomb just transports to assert s; but the most insidious one, it executes Object.freeze () on all typed objects - this means that you cannot just add a field to the object, you cannot even push an array into an array. Do you like immunity? Well, here you are. Together with tcomb you will write immutable code whether you want it or not.


This is a summary of the Hype versus reality report : a year of life with an isomorphic React application (Ilya Klimov)


PS


Now I am transferring my fan project to Flow. I want it strange that the component code is higher than the type declaration for props.


Before:


import React from 'react'
import PropTypes from 'prop-types'
const MyComponent = ({ id, name }) => {
  //...  
}
MyComponent.propTypes = {
  id: PropTypes.number,
  name: PropTypes.string,
}

After:


// @flow
import React from 'react'
const MyComponent = ({ id, name }: Props) => {
  //...  
}
type Props = {
  id: number,
  name: string,
}

But now ESLint is cursing about violating the no-use-before-define rule . And you cannot change ESLint configuration in CRA. And there is a solution, again I use the wonderful react-app-rewired . By the way, he also helped to connect tcomb, all the magic inside config-overrides.js .


And the cherry on the cake. Flow + absolute paths for import:


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

Also popular now: