Blog Post

How To Convert a JavaScript Project With Flow Types to TypeScript

Are you interested in migrating from Flow to TypeScript? Want to update your code incrementally without having to do a total rewrite? We recently underwent this transition so let’s walk through what we did to ease the process here.

At Ginkgo, we write a lot of frontend code and we strive to use modern tooling that helps our developers work more effectively. Most of our frontend apps are written in React with Flow types, which was established as our canonical frontend pattern in late 2016. At the time, I was porting one of our major frontend apps to React from a deprecated framework, and I wanted to introduce static typing to ease the transition. Flow and TypeScript both looked like good options then but we had a hard requirement on using Babel for our compilation, and TypeScript didn’t support Babel at the time, so we decided to go with Flow types.

Flow types have served us well, but we’ve decided to migrate to TypeScript for new code because of the wide community adoption of TypeScript and the correspondingly large amount of third party type definitions. These type definitions really supercharge the development experience with an IDE like VS Code, so we want to take advantage of them for future development. In the rest of this post I’ll describe the way that we are making this transition without doing a total rewrite of our code, which allows for incremental adoption of TypeScript as our new frontend language of choice.

To start with, let's briefly discuss our usage of Babel. We use Babel to transpile our Flow typed JavaScript code with the preset-flow preset, which removes the type annotations and leaves vanilla JavaScript that the browser can run. We separately use “flow-bin” to do type checking.

With TypeScript, we’ll also be using Babel to strip its types and will use tsc separately for type checking. Roughly, our old Babel pipeline looks like this:

Each file with a // @flow comment at the top is processed with the preset-flow babel integration, producing a plain JS file. Let’s take a look at what our builds now look like with both Flow types and TypeScript:

We still produce plain JavaScript, but now files with the TypeScript extension .ts will be handled by Babel’s preset-typescript. In order to achieve this intermediate solution, we’re using the “overrides” feature of Babel, which allows for configuration to be overridden with a pattern match applied to the files going into Babel. Our Babel override section includes a test for all .ts files, which it then processes using preset-typescript rather than preset-flow. This is what our .babelrc file looks like:

{ "presets": [ "@babel/preset-env", "@babel/preset-flow" ], "plugins": [ "@babel/plugin-proposal-class-properties", "@babel/plugin-proposal-object-rest-spread" ], "overrides": [ { "test": ["./src/**/*.tsx?"], "presets": [ "@babel/preset-env", "@babel/preset-typescript" ] } ] }
That takes care of Babel parsing our project files and stripping types to produce plain JS, but it doesn’t actually achieve the type checking we want yet. For that, we installed the typescript package using yarn add --dev typescript @babel/preset-typescript and created a .tsconfig file according to the tsconfig handbook here. Finally, we must make Flow and TypeScript play nicely together, and not produce errors when encountering a file of the other type. To make Flow play nicely with TypeScript files, we add a // $FlowFixMe comment above any TS import, like:
// $FlowFixMe import helloTypescript from './hello_typescript';
Similarly, we have to make TypeScript play nicely with Flow files by setting “allowJs” to false and “noImplicitAny” to false in our tsconfig.json. With these Flow comments and TypeScript settings, we’ve essentially told each of the type checking systems to ignore files of the other type, and treat them as “any” types. This means we lose type checking across file boundaries as we convert files to TypeScript, but it does allow us to incrementally move files over instead of trying to rewrite them all at once.

There are a couple of other tools that we use that need updating to understand TypeScript as well. First off, we update package.json to tell babel to look for .ts files and to include some tsc type checking commands:

{ "scripts": { "build": "babel src -d dist --copy-files --extensions '.js,.ts,.tsx'", "tsc": "tsc --noEmit", "tsc:watch": "tsc --noEmit --watch" } }
Next our .eslintrc file which includes configuration for eslint is also updated to include overrides for TypeScript files:
{ "extends": ["airbnb-base", "plugin:flowtype/recommended", "prettier"], "parser": "babel-eslint", "plugins": ["flowtype"], "env": { "jest": true }, "rules": { "no-process-env": "error", "no-else-return": 0, "import/no-cycle": 0, "import/no-unresolved": 0, "import/extensions": [ "error", "ignorePackages", { "js": "never", "jsx": "never", "ts": "never", "tsx": "never" } ] }, "settings": { "import/resolver": { "node": { "extensions": [".js", ".jsx", ".ts", ".tsx"] } } }, "overrides": [ { "files": ["*.ts"], "extends": [ "airbnb-typescript/base", "plugin:@typescript-eslint/recommended", "prettier" ], "plugins": ["@typescript-eslint"], "parser": "@typescript-eslint/parser", "parserOptions": { "project": "./tsconfig.json" } } ] }
These new ESLint dependencies are installed by running yarn add --dev @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-config-airbnb-typescript. For Jest support, all that’s needed is to install TypeScript types with yarn add --dev @types/jest.

I also discovered that apps created with the create-react-app tool work with both Flow and TypeScript out of the box as of version 2.1.0. All that’s needed is to install the appropriate TypeScript dependencies like yarn add --dev typescript @types/node @types/react @types/react-dom @types/jest and set some overrides in the scripts section of the package.json file:

"tsc": "tsc --noEmit --allowJs false --noImplicitAny false", "tsc:watch": "tsc --noEmit --allowJs false --noImplicitAny false --watch"
These overrides are necessary because create-react-app overwrites any changes to tsconfig.json, so we use command line flags to override the allowJs and noImplicitAny settings.

At this point, Flow and TypeScript talk nicely with each other and all that’s left to do is install our TypeScript types with installs like yarn add --dev @types/node @types/react and then start converting our own files to TypeScript. We’ve been using the flow-to-ts package to convert files to TypeScript as we work on the codebase, which has been working nicely.

This overall strategy of incremental adoption is working well for us, allowing us to use TypeScript for new code without having to overhaul all the old code. Once most of our files are converted, we’ll likely do a final push to get the rest moved over to TypeScript to reach our final goal of having a fully TypeScript codebase:

In this post we’ve seen how to transition a codebase from Flow types to TypeScript, without having to do a total rewrite. In the future we’re looking forward to having fully type safe TypeScript codebases, and we’re working hard to convert files to TypeScript using the methods in this post. Hopefully these techniques will help you too if you have a Flow typed codebase and would like to adopt TypeScript!

(Feature photo by Ravi Pinisetti on Unsplash)



Posted by Dan Cahoon