I’m going to use specific tools and organization patterns in my examples. My goal is to convey the details of this migration as I experienced it. You may not be using the same tools, so take what you can from this article and adapt to your own codebase. The concepts will still hold true.

A Quick Note On Flow <> TypeScript Compatibility

I wanted to make a point here that was hard to fit in any other section. If you are using Flow, you’re using a babel preset/plugin to strip Flow out of your code when you build your application. If you want to start writing TypeScript, you only need to add a few more lines of configuration to make it happen. Add an override to your babel configuration to handle TypeScript code appropriately (see example below).

This will make your application work when executed, barring any additional webpack loader configuration you need to add. The issue we are solving here is not making your application execute correctly, it’s making Flow type checking compatible with TypeScript while we are in the midst of a migration.

The end goal of course is to uninstall Flow from our codebase and be 100% TypeScript 😃

Build the Robot: How to Automate Migrating A Single File

If only there was a machine that could automate moving these leaves around…maybe by blowing air at them…

Migrating code from Flow to TypeScript manually is about as exciting as raking leaves. Good luck to you if you choose to go down this path.

At Zapier, one of our core company values is “Don’t be a Robot, Build the Robot”. Let’s build the robot to do this work so we can spend our time doing more valuable things.

Luckily for us, there is a babel plugin that was written specifically for this job: babel-plugin-flow-to-typescript . Here’s a very quick-and-dirty node script that utilizes this plugin to transform code:

It’s important to give a file the `.tsx` extension if it has JSX syntax. Otherwise, TypeScript will not be happy.

This script will take transform any Flow syntax into TypeScript, and will create the new .ts or .tsx file. There will be minor things you need to touch up in the files processed this way, like whitespacing, but ultimately it will get you most of the way there.

Generate an index.js.flow file

During the migration process you’ll likely run up against a situation where a newly generated TypeScript file is imported by another Flow file, and you’ll want both Flow and TypeScript to both work as intended. They need to be in agreement.

This is essentially what we’re going for with our iterative migration.

Even if you begin your migration from the bottom of your dependency tree, you’ll still run into a situation where your shiny new TypeScript file imports a Flow file.

There is an easy way to get TypeScript to be agreeable with the Flow. For example, if you have a packages/utils/src/capitalize.js file that some TypeScript code depends on, you can create a packages/utils/src/capitalize.d.ts file that looks like this:

I am quite amused with myself that I created a one line gist.

On the other hand, if you import a TypeScript component in a Flow file, it will cause Flow’s type checking to break so the component is of type any . You may even get new Flow errors that weren’t there before. We need to generate an index.js.flow file to pair with this migrated code so Flow knows what to expect. This file will contain Flow type definitions for the associated TypeScript files.

Again, you could do this manually, but what if someone makes a change to the TypeScript code post-migration? That contributor could forget to update the index.js.flow file and accidentally break something. Even worse, Flow may let it slide silently!

So how do we generate the index.js.flow ? There are two steps involved:

First, generate a .d.ts file with all of the contents of the package’s publicly exported types. I am using dts-bundle-generator , however in hindsight I think this can be done with tsc --declaration --emitDeclarationOnly combined with concatenating all .d.ts files generated through a cat file1.d.ts file2.d.ts command. Your mileage may vary. Generate a Flow definition file for the .d.ts file generated on step 1. I found flowgen able to do the job, albeit with some quirks that made me need to rework a few type definitions in my TypeScript code. Tell Flow about this file so it’s aware. In your .flowconfig you will need to add this file under your [libs] section. I tried adding module.name_mapper.extension='ts' -> '<PROJECT_ROOT>/index.js.flow to my .flowconfig but it didn’t seem to work as expected, but I am migrating off of a relatively old version of Flow.

Automating the index.js.flow Generation

While the code migration doesn’t need to be done more than once, you’re going to want to regenerate these index.js.flow files every time the underlying TypeScript code is changed. Remember — the contents of the index.js.flow file represent the type constraints on that code itself. If you add a new prop to a component and don’t update the index.js.flow file, you now have two realities for how that component will be consumed. You’re going to run into an error in Flow or worse, production! ☠️

A way to solve this is to make sure the index.js.flow files are generated whenever something changes. I found the best way to do this is to have a pre-commit hook run on the developers machine so code never even makes it up to origin with incorrect Flow typings. You can even loop through the staged files and whatever command you come up with for each file. If files are organized together in a package, you can run it once for that entire package. Just make sure your index.js.flow files are always in sync. If you believe someone may commit some code with --no-verify to bypass this, add a step in CI that runs this command and checks to see if there is a diff between the output and what is in master.

Now that we’ve figured out how to migrate Flow code to TypeScript, and generate an index.js.flow for that file so no type constraints are broken in existing code, let’s talk about migration strategies.

Strategy 1: Migrating one Lerna/Yarn Workspace Package At a Time

Using lerna or yarn workspaces to organize your code is a common pattern that lots of projects are using these days.

Here’s an example project layout:

I feel like every project has a `utils` folder. It’s like the junk drawer I had in my house growing up.

Even in this simple example, you can imagine a scenario where all packages rely on utils , most rely on ui-components , and state-or-entities-or-whatever could have circular dependencies with utils . This stuff gets gnarly fast.

One strategy we discovered is to migrate one package at a time.

If a package has an index.js that exports all publicly facing exports (functions, variables, etc), you will need to generate one sibling index.js.flow for that file. This file will represent the Flow-parsable type representations for your TypeScript code. This may also help your mental model of knowing what is TypeScript and what is Flow in your codebase.

Big indicators that Strategy 1 is a good plan to migrate a package:

It has relatively few public exports in the top-level index.js It’s small enough that you could imagine reviewing the entire package in one sitting without a gun to your head. If you’re not sure about that, run the migration script on the entire package and take a look at the diff. Would you expect someone else to review that in good faith?

If that’s the case, go for Strategy 1 to migrate that package.