fbpx
General

Migration Of An Application Frontend To TypeScript

In Loadero we always look for ways to improve our product and make it more robust, secure, and maintainable. As we add more features to our product, the complexity of our code base increases and it makes it more difficult to add or refactor the code without introducing regressions of the functionality. Since our frontend was written in plain Javascript and React, there was no way to ensure type safety of passed data between components and functions.

With the complexity of our code, we reached the point where there was no confidence in changing some parts of the application without introducing new bugs. At that point, we decided to migrate our frontend codebase into TypeScript. It is the de-facto option of choice, and no other strongly typed programming languages or static type checkers for Javascript come even close to TypeScript in terms of the size of the community, features, speed, and IDE support. Such a migration is a complex task, so we start with some preparation.

Write guidelines and plan migration

Before jumping into converting all of our code into Typescript, we created a plan. One of the important things we wanted to do first was to ensure that the style of our code is consistent. This is very important as we want to keep our code predictable, so anyone would be able to look at it and understand what’s going on there. So that’s what we did – we took a look at our existing code, put our heads together and wrote guidelines that explain standards all engineers should adhere to when they write code in TypeScript. The main points we have put in our guidelines were: 

  • Types and interfaces
  • Eslint and Prettier configurations
  • Functional component signatures
  • File naming conventions
  • Hook definitions
  • Typing of Redux APIs

Once we had our new coding guidelines created, we proceeded with auditing all our files to create a strategy for the migration. We checked all imports and decided which files needed to be converted first. We wanted to convert all dependencies first like utility functions, API wrappers, hooks, configs and others, so TypeScript components don’t rely on unconverted files. Figuring out the strategy for migration didn’t take a long time as it was pretty obvious from the code what needed to be converted first.

At the time of migration, we were also shipping new features and refactoring our existing code, and all this new code was written in TypeScript. The great thing about TypeScript is that it allows us to have JS and TS files coexisting together without any issues, allowing us to implement the migration incrementally, step by step.

Setup the project

Since our project was bootstrapped with Create React App, it was very easy to configure the compiler to use TypeScript. All we had to do was add tsconfig.json file to the root of our project with some predefined rules. To improve build times and prevent issues with incompatible external library types, we enabled skipLibCheck flag in the tsconfig.json.

Once the configuration file was set up, we updated our ESlint/prettier files to use TypeScript. There is not much to be said about configuring those apart from adding some TS-related rules and installing dependencies like @typescript-eslint/eslint-plugin @typescript-eslint/parser so your prettier and ESLint work with TypeScript.

Migration

Having all the required configuration of the project done we started the migration. We have planned all required steps and proceeded with converting javascript files one by one. As we planned, we started converting files that have no dependencies on other files and gradually moved to files that are located closer to the root of the project. By following this strategy, we minimized the risk of having lots of conflicts and TypeScript errors between different files.

While doing migration we have spotted some bugs and improved the quality of our code. The majority of bugs could have been avoided if we had a type safety in place. The examples of those bugs were: missing or incorrect arguments while calling functions, accessing null values and missing props in components. All of those issues have become apparent after compiling the code into TypeScript. With type safety in place, we can define contracts by using interfaces and guarantee that components receive correct props, thus eliminating potential bugs. 

One of the great improvements we have introduced to our projects was to implement type-safe translations. Since TypeScript is now able to parse key-value dictionaries in JSON files, we can determine the type of translation dictionaries. This allowed us to validate all translation keys during the compilation stage and provide some autocompletion of keys in our code editor.

Caveats

Of course, the migration didn’t go 100% hassle-free as there were a lot of changes to be done. Some parts of our codebase went much quicker, but some required more thinking and refactoring before it could be converted to TypeScript. To type our low-level API wrappers we had to restructure them a bit and heavily rely on generics so our wrappers can stay 100% reusable and work with any data they pass through. Even though we followed our plan to minimize conflicts and errors between files, we still spent quite a lot of time fixing tricky type errors while doing the migration. With complex file dependencies, you start revealing type errors of already converted files only when you start converting dependent files to TypeScript. 

After putting lots of our engineer’s time and effort into the migration, we can certainly say that the quality and robustness of our frontend code are much greater now. The refactoring of our code that was already written in TypeScript has already become much easier, safer, and requires less time. It does take more time to write code in TypeScript compared to plain Javascript, but then you get that time back when you start refactoring your code.