Mixmax is migrating our JavaScript (JS) code to TypeScript (TS). We already have an article about migrating from Flow to TypeScript, but until this year, our migrations were instant. This means that when we migrated from Flow, we migrated the JavaScript code to TypeScript all at once. But that’s not always the case for migrations—and for larger and more valuable projects, instantaneous migrations might be impossible.
Our largest service is Mixmax dashboard and because migrating it seemed risky, we didn’t consider doing it for a long time. But at some point, as more and more engineers joined the company (including myself), it became obvious that we needed to do it. So the questions became, how can we do it? What is the least risky way?
Incremental Migration
Because the project was large and some of the technologies used were becoming less relevant—we still use Backbone views, models, and routers that eventually render React applications—migrating everything to TypeScript at once didn’t make much sense. Firstly, because we were moving away from these technologies, and, secondly, because a proper migration may have taken us months, or even years!
It’s important to note that for such a crucial service we didn’t want to use any of any in our TypeScript code, so we didn’t use automated tools like flow-to-ts. Unfortunately, as the project was huge, we didn’t even have full support of Flow, so that option wasn’t considered.
After doing some testing, we decided to proceed with an incremental approach. We migrated JavaScript code partially: file by file, module by module. In this way the migration didn’t affect the whole application, so risks of causing errors because of these changes were low.
Currently, for all new features, files, and modules, we strive to create TypeScript files instead of JavaScript files. And if we refactor something, we try as much as possible to move old files to TypeScript.
How We Did It
As it turned out, adding partial TypeScript support was simpler than we thought!
Mixmax uses webpack as a bundling tool, so besides adding TypeScript we also installed a ts-loader. The only changes we made to our webpack.config.json were adding .ts and .tsx to extensions.resolve and adding ts-loader to module.rules.
```
module.exports = {
// ...
resolve: {
extensions: ['.js', '.ts', '.tsx'],
// ...
},
module: {
rules: [
// JS files
{
test: /\.js$/,
use: 'babel-loader',
exclude: /node_modules/,
},
// TS files
{
test: /\.tsx?$/,
use: 'ts-loader',
exclude: /node_modules/,
},
],
},
};
```
As a result, today we use Babel for JavaScript files, as we did before, and we use ts-loader for TypeScript files.
Notes on tsconfig.json
Before TypeScript, we had jsconfig.json with our JavaScript configuration for our IDE. Most of all, it was useful for defining our aliases. Clicking on ~/utils/something sent us to the needed file.
After adding tsconfig.json for TypeScript files, we noticed that our aliases in JavaScript files stopped working. As it turned out, it’s impossible to have both jsconfig.json and tsconfig.json for the same directory. Because TypeScript config has a higher priority and we didn’t include JavaScript files there, JavaScript stopped understanding our aliases—it just couldn’t find its configuration.
The solution for this was to have one configuration file for both TypeScript and JavaScript. If we include JavaScript files and set the allowJs option to true, JavaScript files will eventually find tsconfig.json and use it. So here’s how a part of our configuration looks:
```
{
"compilerOptions": {
// ...
"noImplicitAny": true,
// This and JS files in `include` section allow
// JS files to find this config
// Make sure that you have enough `max_old_space_size`
// option set for building
"allowJs": true,
// For not taking output files as input
"outDir": "public",
// Unfortunately, there will be too many type errors
// if we check JS files
"checkJs": false
},
"include": [
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.js"
],
"exclude": [
"node_modules",
"public"
],
// ...
}
```
Declaration Files vs. any
Often when migrating a JavaScript file to TypeScript, the compiler complains about missing types from other JavaScript files that are imported. To fix that, we could either migrate those files to TypeScript (which would have been a lot of work, as they depend on other files too) or create declaration files. As mentioned earlier and as you may have noticed from our tsconfig.json, we don’t use any type, so we decided to go with the declaration files approach. In this way we were forced to create declarations in order to have a fully typed codebase for TypeScript files. The benefit is that we then wouldn’t need to migrate everything at once; we only needed to create declarations for the objects that we use.
So for each importing of a JavaScript file, we needed to have a declaration file for the module, and creating the declaration files helped a lot. First of all, we were able to sort out needed cases of how the imported module behaves even before using it. And secondly, creating declaration files was also excellent for understanding the codebase better, if you were not already fully familiar with it.
Lessons Learned
- It is possible—and easy—to migrate from JavaScript to TypeScript incrementally. And it’s certainly worth it!
- It’s not possible to have multiple TypeScript or JavaScript configs in the same module. Use tsconfig.json for both TypeScript and JavaScript files with the allowJs option equal to true.
- Make your TypeScript configuration stricter and use declaration .d.ts files instead of using any. This way, you can be sure about your types and your subsequent module migrations will be easier.
If you’re still on JavaScript, TypeScript is waiting for you! 😄
Interested in joining a team that loves to improve their technology? Visit Mixmax Careers.