Experience translating a large project from Flow to TypeScript

Directum logo



JavaScript is one of the languages ​​with dynamic typing. Such languages ​​are convenient for rapid application development, but when several teams take on the development of one large project, it is better to choose one of the type checking tools from the very beginning.



You can start developing TypeScript code or include it in a Flow project. TypeScript is a compiled version of JavaScript developed by Microsoft. Flow, unlike TypeScript, is not a language, but a tool that allows you to analyze code and check types. There are many articles and videos on the web about these approaches, as well as a guide on how to start using typing. In this article we would like to tell you why Flow did not suit us, and how we started to switch to Typescript.



A bit of history



In 2016, we started developing a React / Redux-based web client for our ECM system. Flow was selected to test typing for the following reasons:



  1. React and Flow are products of the same company Facebook.
  2. Flow developed more actively.
  3. Flow is easily integrated into the project.


But the project grew, the number of development teams increased, and a number of problems appeared when using Flow:



  1. Background type checking Flow used too many PC resources. As a result, some developers turned it off and started checking as needed.
  2. There were situations when it took as much time to bring the code into line with Flow as writing the code itself.
  3. The code began to appear in the project, necessary only to pass the Flow test. For example, double checking for null:



    foo() { if (this.activeFormContainer == null) { return; } // to do something if (this.activeFormContainer != null) // only for Flow this.activeFormContainer.style.minWidth = '100px'; }
          
          



  4. Most developers used the Visual Studio Code code editor, in which Flow does not have as good support as TypeScript. Auto-completion (IntelliSense) did not always work during development, and code navigation was unstable. I would like to have the same ease of development as when writing in C # in Visual Studio.


Some developers had the idea to try switching to TypeScript. In order to test the idea of ​​the transition and convince the management, we decided to try the prototype.



Prototype



We wanted to test two ideas on prototypes:



  1. Try to translate the whole project.
  2. Set up the project so that you can use both Flow and Typescript in parallel.


For the first idea, a utility was needed that would convert all the project files. On the network found one of these. Judging by the description, she could translate most, but some of the changes would have to be edited by ourselves, or the utility itself should be added. We were able to convert a test project with a small number of files. But the real project could not be compiled, it would have been necessary to edit too many files. We decided not to continue in this direction, as:



  1. There was still much to be done! And while we will finalize the project, the remaining teams will continue to develop new functionality, edit bugs, write tests. In addition, it would take a lot of time to merge files.
  2. Even if we translated the project in this way, then how much work our testers would have to do!


Although we abandoned this option, we gained useful experience on it. It became clear the approximate amount of work that needs to be done to translate each file. Here's what the translation of a simple React component looks like.

Comparing Flow and TypeScript Code





As you can see, there are not many changes. Basically, they are as follows:





The implementation on the second idea would allow further development, but already on TypeScript, and in the background to slowly translate the existing code base. This provided several advantages:



  1. Easy to translate, without fear of missing something.
  2. Easy to test.
  3. Easy to merge changes.


But it was not completely clear whether the project could be configured to work with two types of typing in parallel. A search on the Internet did not lead to anything concrete, so they began to sort it out themselves. In theory, the Flow analyzer only checks files with the js / jsx extension and containing a comment:



 //@flow  /* @flow */
      
      





For the TypeScript compiler, files must have the extension ts / tsx. From which it follows that both approaches to typing should work simultaneously and not interfere with each other. Based on this, we set up the project environment. Using the experience of the first prototype, we translated a couple of files. Compiled the project, launched the client - everything worked as before!



Green light



And one fine day - the day of sprint planning, our team has a User Story “Start Switching to TypeScript” in the backlog, with the following list of works:



  1. Set up webpack.
  2. Configure tslint.
  3. Set up a test environment.
  4. Translate files to TypeScript.


Webpack setup



The first step is to teach webpack how to handle files with the ts / tsx extension. To do this, we added a rule to the rules section of the configuration file. Originally used ts-loader:



 // webpack.config.js const rules = [ ... { test: /\.(ts|tsx)?$/, loader: 'ts-loader', options: { transpileOnly: true } } ];
      
      





To speed up the assembly, type checking was turned off: transpileOnly: true



, because The IDE already indicates errors while writing code.



But when we started translating our Redux actions, it became clear that they needed the babel-plugin-transform-class-display-name plugin to work. This plugin adds a static displayName property to all classes. After translation, only ts-loader began to process actions, and this did not allow babel plugins to be applied to them. As a result, we abandoned ts-loader and extended the existing rule for js / jsx by adding babel / preset-typescript:



 // webpack.config.js const rules = [ { test: /\.(ts|tsx|js|jsx)?$/, exclude: /node_modules|lib/, loader: 'babel-loader?cacheDirectory=true' }, ... ];
      
      





 // .babelrc.js const presets = [ [ "@babel/preset-env", { "modules": !isTest ? false : 'commonjs', "useBuiltIns": false } ], "@babel/typescript", "@babel/preset-react", ];
      
      





For the TypeScript compiler to work correctly, you need to add the tsconfig.json configuration file, it was taken from the documentation.



Configure Tslint



Code written using Flow was additionally checked using eslint. TypeScript has its counterpart, tslint. Initially, I wanted to transfer all the rules from eslint to tslint. There was an attempt to synchronize rules through the tslint-eslint-rules plugin, but most of the rules are not supported. It is also possible to use eslint to check ts files using typescript-eslint-parser. But, unfortunately, only one parser can be connected to eslint. If you use only ts-parser for all kinds of files, a lot of strange errors appear in both js files and ts. As a result, we used the recommended set of rules, expanded to our requirements:



 // tslint.json "extends": ["tslint:recommended", "tslint-react"]
      
      





Translate a file into TypeScript



Now everything is ready, and you can start translating files. To begin with, we decided to transfer a small React-component, which is used throughout the project. The choice fell on the “Button” component.



Buttons in the project



We encountered a problem during the translation process: not all third-party libraries have TypeScript typing, for example, bem-cn-lite. On the TypeSearch resource from Microsoft, the type library for it could not be found. for almost all the necessary libraries, we found and connected ts type libraries. One solution was to connect via require:



 const b = require('bem-cn-lite');
      
      





But at the same time, the problem with the lack of types was not solved. Therefore, we generated a “stub” for the types ourselves, using the dts-gen utility:



 dts-gen -m bem-cn-lite
      
      





The utility generated a file with the extension * .d.ts. The file was placed in the @types folder and configured tsconfig.json:



 // tsconfig.json "typeRoots": [ "./@types", "./node_modules/@types" ]
      
      





Further, by analogy with the prototype, we translated the component. Compiled the project, launched the client - it all worked! But the tests broke.



Test environment setup



To test the application, we use Storybook and Mocha.



Storybook is used for visual regression testing ( article ). Like the project itself, it is built using webpack and has its own configuration file. Therefore, to work with ts / tsx files, it had to be configured by analogy with the configuration of the project itself.



While we used ts-loader to build the project, we stopped running Mocha tests. To solve this problem, add ts-node to the test environment:



 // mocha.opts --require @babel/polyfill --require @babel/register --require test/index.js --require tsconfig-paths/register --require ts-node/register/transpile-only --recursive --reporter mochawesome --reporter-options reportDir=../../bin/TestResults,reportName=js-test-results,inlineAssets=true --exit
      
      





But after switching to Babel, this could be eliminated.



Problems



In the translation process, we encountered a large number of problems of varying degrees of complexity. They were mainly related to our lack of experience with TypeScript. Here are a few of them:



  1. Import components / functions from different file types.
  2. Translation of higher order components.
  3. Loss of change history.


Import components / functions from different file types



When using components / functions from different file types, it became necessary to specify the file extension:



 import { foo } from './utils.ts'
      
      





This allows you to add valid extensions to the webpack and eslint configuration files:



 // webpack.config.js resolve: { … extensions: [ '.tsx', '.ts', '.js' ] }
      
      





 // .eslintrc.js "import/resolver": { "node": { "extensions": [ ".js", ".jsx", ".ts", ".tsx", ".json" ] } }
      
      





Translation of higher order components



Of all file types, the translation of Higher-Order Component (HOC) caused the most problems. This is a function that takes a component at the input and returns a new component. It is mainly used for reusing logic, for example, it can be a function that adds the ability to select elements:



 const MyComponentWithSeletedItem = withSelectedItem(MyComponent);
      
      





Or the most famous connect, from the Redux library. Typing of such functions is not trivial and requires connecting an additional library to work with types. I won’t describe the translation process in detail, as you can find many manuals on the subject on the net. In short, the problem is that such a function is abstract: any component with any set of properties can accept input. It can be a Button component with title and onClick properties or a Picture component with alt and imgUrl properties. The set of these properties is not known to us in advance; only those properties that the function itself adds are known. In order for the TypeScript compiler not to swear when using components obtained with the help of such functions, it is necessary to "cut" the properties that the function adds from the return type.



To do this, you need:



  1. Pull these properties into the interface:



     interface IWithSelectItem { selectedItem: number; handleSelectedItemChange: (id: number) => void; }
          
          



  2. Remove all properties that enter the IWithSelectItem interface from the component interface. To do this, you can use the operation Diff <T, U> from the utility-types library.



     React.ComponentType<Diff<TPropsComponent, IWithSelectItem>>
          
          





Loss of change history



To work with sources, for example, code review, we use Team Foundation Server. When translating files, we came across one unpleasant feature. Instead of one changed file, two appear in the request pool:





This behavior is observed if there are many changes in the file (similarity <50%), for example, for small files. To solve this problem, we tried to use:





But, unfortunately, both approaches did not help us.



Summary



Use Flow or TypeScript - everyone decides for himself, both approaches have their pros and cons. We chose TypeScript for ourselves. And you were convinced from your own experience: if you chose one of the approaches and suddenly realized, even after three years that it does not suit you, then you can always change it. And for a smoother transition, you can configure the project, like us, to work in parallel.



At the time of writing, we have not yet fully switched to TypeScript, but we have already rewritten the main part - the "core" of the project. In the code base, you can find examples of the translation of all kinds of files, from a simple react component to higher-order components. Also, training was conducted among all development teams, and now each team, as part of its task, transfers part of the project to those duties.



We plan to complete the transition before the end of the year, translate tests and a storybook, and perhaps even write some of our tslint rules.



According to my personal feelings, I can say that development began to take less time, type checking is done on the fly, while not loading the system, and error messages for me personally became more understandable.



All Articles