Converting From Flow To TypeScript

January 25, 2025

This week I took on a task that I didn't ever expect to perform, at work or with my personal projects - converting a Flow project into TypeScript.

The first part of the challenge was a pretty simple one - understanding what the hell Flow even is.

I'd heard of Flow previously, but never actually seen it in the wild anywhere. We had considered it back when I was working at Savedo and we started an application rewriting project from Angular to React. Back then, we had the choice between Flow and TypeScript and our team was split. This was in 2018 and at that time there were arguments for and against each.

Having had no experience in either, I didn't have a horse in the race, so went with the majority rule, which was TypeScript (thankfully!).

Fast forward to 2025, it's safe to say that Flow has dropped off, and TypeScript is now the standard in type-safe JavaScript code. But that still left me not really knowing how Flow worked and where to start with converting an entire project from Flow to TypeScript.

Flow, like TypeScript, provides type-checking and type safety for JavaScript projects. Its syntax is similar to TypeScript in how types are declared and used, but from what I've seen it lacks the ability to type nested objects accurately. It works by using regular .js files with a // @flow annotation at the beginning of the file. This denotes that this JavaScript contains Flow syntax and should be compiled as such.

With a surface level understanding and a shed load of files to convert, I began with a brute force approach. I introduced the TypeScript npm package and initialised it, added a tsc script in the package.json that ran TypeScript, and ran it to see what errors it would throw out.

Initial file conversion

Running TypeScript on the whole project before any conversion threw up some errors. It also hid a lot of errors from files that were not being checked by TypeScript. The main issue is that TypeScript recognised the similarities of the Flow syntax and gave an error that TypeScript features are not available in JavaScript files. I started by converting those file from .js to .ts. A simple start, but one that eventually led me down a rabbit hole that somehow came up in the right place in the garden.

I continued converting files as TypeScript warned me of the issues, and eventually I'd converted about a third of the project. Despite the fact that TypeScript was now compiling, the project clearly wasn't going to work as intended.

Tackling the unit tests

The next angle of attack was to convert the tests. Switching from .js to .ts and running the whole test suite threw up more errors than I'd ever seen before. There were literally hundreds of errors that both Jest and TypeScript were giving me. My plan was simple, start from the last and wok my way through. Most of this was pretty standard work, and I decided early on not to figure out too many new types on the way. Where Flow allowed function arguments to be untyped, I set them as any and went on my merry way. This was no time to be inspecting every file and class in the project. I wanted to get it done so that the tests were passing and the project was compiling error-free.

This part was still a huge challenge. I came across an adapter class which had 3 subclasses for different integrations with 3rd party libraries. Each of these 3 subclasses implemented one of the functions differently, with different arguments of different types and different return types - nightmare. It took me a good while to unpick this function and leave it in a place where at least the compiler wasn't complaining. I made a small misstep here, which we'll come back to later.

Once the mightmare class was out of the way, I was about half way done with the overall project conversion.

Converting customer configs

I didn't realise it at the time, but I stumbled upon a huge customer config directory which contained many mostly duplicated files. It was going to be painful to rename them all by hand, so I took a step back and tried to automate my way out of this pickle. I didn't much fancy spending 30 minutes just renaming files. Too monotonous.

Instead I wrote a simple bash script which I executed on the files, which saved me about two thirds of that time and came with added benefits later when I could reuse it. It looked like this:

convert_ts.bash

#!/bin/bash

for file in ./src/customers/*.js

do
  echo "${file%.*}"
  mv "$file" "${file%.*}.ts"
done

It took less than a second and saved me a lot of time.

Controlling end-to-end tests

Once the mammoth customers were done, a sneaky little directory that had been hiding from me up until now reared its ugly head - the end-to-end tests. The project uses Cypress to run through some common workflows and critical user paths. I converted those to .ts with a modified version of my script from above and was suddenly stopped in my tracks.

It was at this point that I needed to tweak my TypeScript configuration. Cypress was unhappy with the violent switch from the comfort of JavaScript to harsh, cruel, unforgiving world of TypeScript. It complained at me about basically everything. I realised I couldn't rely on my previously created tsconfig.json to satisfy Cypress, so I had to create a new one within the cypress directory. It was quite basic and looked like this:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["es5", "dom"],
    "types": ["cypress", "node"]
  },
  "include": ["**/*.ts"]
}

The main one for the project looked like this, in case you're interested:

{
  "compilerOptions": {
    "target": "esnext",
    "lib": [
      "dom",
      "dom.iterable",
      "esnext"
    ],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": false,
    "outDir": "./dist",
    "types": [
      "node",
      "jest"
    ]
  },
  "include": [
    "src"
  ]
}

With the Cypress tests now satisfied from a type perspective, I ran them to check whether I'd buggered anything else up with the way the project worked. At this point, it became clear to me that I'd missed a rather glaringly obvious task - switching over the Webpack compilation step to accommodate TypeScript. tsc was doing a great job checking that the files compiled properly, but I still hadn't made sure the project as a whole was compiling into its target files correctly.

This was a little trickier than anticipated. Not only was this the first time I'd converted a project into TypeScript, this was actually the first time I'd worked on this particular project at all. It was completely foreign to me.

At this point I had to take a step back again and figure out what the project was actually supposed to be doing from a high level. Quickly I realised that the customer files needed to be output, as well as another script. All this was in the Webpack config and with a quick flick through the documentation, I managed to switch over the source files from .js to .ts and add the ts-loader rule into compilation process to deal with those TypeScript files.

And with that the Cypress tests were passing.

Making a mess

Unfortunately, the Cypress tests didn't tell the complete story. The class that was particularly tricky to convert contained one adapter for a third party advertising service. Unfortunately this integration wasn't tested and I missed it in my manual tests due to the fact that it's mocked in most cases. This is to prevent test data from running through the actual integration. Regrettably, this caused a outage that had to be fixed shortly after deploying the change to TypeScript. Fortunately, it was caught quickly and the problem was swiftly identified, so there wasn't too much fallout from the outage.

Overall, this was a relatively smooth conversion task. It was helped by not being too strict with any types and using a script to facilitate the file renaming process.

Are you looking to take the next step as a developer? Whether you're a new developer looking to break into the tech industry, or just looking to move up in terms of seniority, book a coaching session with me and I will help you achieve your goals.