📜 ⬆️ ⬇️

Flow + tcomb = typed JS

Sooner or later, everyone comes to the conclusion that we need strong typing. Why? Because the project grows, overgrows if; Functional programming - the whole function is not true, I just had the console say "undefined is not a function". These problems appear more and more often, it becomes harder to keep track of, the question arises - let's be strongly typed, at least at the stage of writing the code will prompt.


You know the ads: 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 not correct from the TypeScript point of view. This does not mean that TypeScript is a bad language, but to advance on the idea of ​​a superset, and let me down so, TypeScript I did not expect.


As soon as you eliminate TypeScript, there remains exactly one alternative - Flow. What can I say about Flow? Flow mega-cool with what makes you learn the OCaml type system, whether you like it or not. Flow is written in OCaml. It has much stricter and much more powerful type inference than TypeScript. You can rewrite the project on Flow partially. The number of bonuses that Flow brings you is difficult to describe. But, as always, there is a couple of "but".


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, |} 

Pipe "|" inside curly brackets mean strict type - only these fields and nothing more. At the entrance of the reducer are required to come only such actions:


 type ActionsType = | BuyTicketActionType | BuyTicketFailActionType ; 

Flow beautifully verifies this. It would seem that everything is excellent, but no. Flow only works with types. You 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 a type is the value of this constant; the chicken and egg problem, the constant is already a code that should be typed, and types should not interact with the code. Therefore, one has to say that the BuyTicketActionNameType type is some kind of string, and further that the BUY_TICKET constant is of the same type, solely in order to check that the string is the same. Slightly perversion.


What else. These strict types are very cool, very conveniently allow you to identify typos and stuff; only they do not understand the spread-operator :


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

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


But the main problem of Flow, 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 there is not exactly what you expect. For example, you expect that the user will always come to the component, and sometimes null comes to it - everything, you did not put a question mark, Flow does not catch it. That is, the usefulness of Flow begins to fall as soon as you start winding it up on an existing project, where you have an understanding of what is going on in your head, but in reality this does not always happen the way you intended.


There are still backend-programmers who like to change data formats and not notify you about it. We begin to write JSON schemas to validate data at the input and at the output, so that if something happens, we can say that the problems are on your side.


But as soon as you start writing JSON schemas, you get two sources of typing: JSON schemas and Flow. Keeping them in a consistent state is the same myth as about supporting the relevance of JSDocs. They say that there are programmers somewhere who support JSDoc in absolutely up-to-date state, but I haven’t met them.


And here comes to the aid the most amazing plugin, which for me is a killer feature, why now I 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 checks in runtime. That is, when you describe a type system, your functions in the development mode will automatically check the input data and output data for type matching. No matter where you got this data from: parsing JSON, and so on, and so on.


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


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


This is a synopsis of a part of the HYIP report against reality: a year of life with an isomorphic React application (Ilya Klimov)


PS


Now I am translating my fan project to Flow. It would be strange if the component code was 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 swears at the violation of the no-use-before-define rule. And you cannot change the configuration of ESLint in CRA. And there is a way out, again apply fine 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 

')

Source: https://habr.com/ru/post/326538/


All Articles