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
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.
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.
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.