December 13, 2021

TypeScriptifying Our Flow Code: Tools and Lessons Learned

TypeScriptifying Our Flow Code: Tools & Lessons Learned | Mixmax

Much of Mixmax's codebase was originally written in Flow, the most popular type-checking Javascript compiler at the time. In the years since, TypeScript emerged as the community favorite, with many Javascript libraries, frameworks, and tools making the switch. For Mixmax, we followed suit.

Following a successful experiment conducted as a “10% time” project, we decided to kickstart the migration to TypeScript during our Mixmax Hack Days. For two days, four engineers set up the necessary configurations, chose tooling, and converted the first dozen Flow repositories to TypeScript out of the fifty-eight identified as most frequently updated.

After the Hack Days, tasks to convert the remaining repositories entered our regular sprint plans and, at the time of writing this post, thirty-eight of those repositories have been converted to TypeScript with no hiccups and great success.


TypeScript Configuration

Choosing our tsconfig.json settings required some trial-and-error. In the end, we chose rather strict type checks, while still balancing the strictness with the amount of work required (in other words, the number of type errors that had to be fixed). We did not initially include strictNullChecks, but we have since updated to include it, as the safety it provides is invaluable. Here are our current type checking settings, as collected in our internal ts-config module:

  "strict": true,
  "noImplicitReturns": true,
  "exactOptionalProperties": true,
  "noFallthroughCasesInSwitch": true,
  "noImplicitOverride": true,
  "noUncheckedIndexAccess": true,
  "noUnusedLocals": true,
  "noUnusedParameters": true

As for module management, we settled for "module": "commonjs", which is the recommended setting for TypeScript on Node, with "esModuleInterop": true to ensure interoperability with existing code.

The downside is that TypeScriptifying a module now becomes a breaking change, as any require() will have to be transformed into a require().default, and any mocks of the TypeScriptified module in the dependent module will need to return an object like { default: , __esModule: true }. This is an annoyance, but has not been a problem so far.


The TypeScriptification Process

We included a command in our CLI tools which, when run in the root of any Flow repository, will:

  • Remove all Flow, Babel, and Rollup dependencies and configurations.
  • Install all TypeScript dependencies, including @types/jest, @typescript-eslint/eslint-plugin, @typescript-eslint/parser, and our internal ts-config module.
  • Update our ESLint, GIT, and Jest configurations to work on .ts files instead of .js.
  • Use the excellent JSON manipulation package json to update the build, lint, and watch commands, as well as the files and main entries in the project’s package.json.
  • Initialize tsconfig.json and tsconfig-lint.json (the difference being that we lint tests and mocks, but we don’t want to include them in the distribution).
  • Use flow-to-ts to convert Flow to TypeScript:
    flow-to-ts --write --delete-source `find src test spec -name '*.js'`.
  • Use jscodeshift to convert CJS imports to ES6 imports, using transform commonjs-to-es-module-codemod.
  • Fix eslint errors and pretty-print by running npx eslint –fix.

Flow-to-ts does an excellent job at converting type annotations. So far, we’ve found only one bug, plus a failure to translate an opaque type. Since TypeScript does not support those, they need to be simulated.

After this TypeScriptification pipeline is run, it will be time to fix any type errors (TypeScript is much better than Flow at inferring types, so it reports many more errors). For large repositories, these can often be counted in the hundreds, or even thousands. Fortunately, fixing them is usually straightforward, although tedious.

We’ve found it efficient to fix errors in pairs by sharing each other’s screens and going through the errors one by one. In this process, one person fixes those errors that affect only types (and can thus be validated by checking that they don’t change the output of the TypeScript compiler), while the other fixes those which require actual code changes. The coder who is not implementing a fix will just watch, so that the changes will be reviewed by the time they are committed.


Lessons Learned

We came out of our experience switching from Flow to TypeScript with a few distinct lessons learned. If you’re considering such a migration, we hope that you find the following advice useful:

  1. Be ambitious with the type checking: Even if it seems like a humongous job, enable strict, including strictNullChecks. But you can still try to balance cost and benefit. We learned that noImplicitAny probably doesn’t pay off, and useUnknownInCatchVariables definitely doesn’t, at least for us.
  2. Spend some extra time with the first few modules, experimenting with different type checking configurations.
  3. The most important lesson? DO IT. Completing this process now is better than doing so in the future. And while it takes some time to get started, you’ll immediately begin reaping the benefits, including faster and safer development with stronger type checks, preventing a whole class of relatively trivial errors and guiding our IDEs. I personally find it removes a lot of stress and helps for focusing on the functionality at hand, away from the nitty gritty implementation details.


Would you like to join us in modernizing our codebase to improve the quality and speed with which we deliver functionality to our customers? Check our careers page.

Ready to transform your revenue team?

Request a demo