In one of our biggest projects, we develop and maintain a React web application. Last year, we decided to migrate it to TypeScript. How did it go? How did we migrate JavaScript to TypeScript? Was it worth it? What struggles we met and are we still having any issues? I’ll try to address these questions in this article.
Disclaimer: I’m not describing the migration process step-by-step or TypeScript itself in this article. You can find many resources on that online, including official TS docs.
JavaScript to TypeScript migration strategy
If you search online, you will find many possible strategies of migrating JavaScript to TypeScript. We considered two of them:
- converting the whole JavaScript codebase to TypeScript at once
- starting to use TypeScript compiler and migrate step-by-step.
After a short discussion, we decided to go with the step-by-step migration strategy. Why? First, it’s less time-consuming. Today, we know that it would kill us if we decided to convert all our JS codebase to TS at once. It would also freeze our codebase for a while – until every .JS(X)
file is renamed to .TS(X)
and all compile errors are fixed.
Thanks to the fact that TypeScript is a superset of JavaScript, .JS(X)
and .TS(X)
files can co-exist even if you switch to use TypeScript compiler. That’s because every JavaScript is also a valid TypeScript code and TS compiler can handle it.
That’s very comfortable. Just think about that – you can switch your project to TypeScript without actually having to change any code. We decided to immediately re-write a few of our recently-added functionalities to TypeScript and convert the rest gradually.
What TypeScript gave us
Before I tell you about the struggles, I want to really encourage you to migrate JavaScript to TypeScript. After 1 year since we switched to TypeScript, I’m telling you it’s the best thing you can do to your web app ?
Let’s see what you’ll gain if you decide to migrate JavaScript to TypeScript.
Types in new code
As soon as you switch your compiler to ts-loader, you can start creating files with TypeScript extensions (.TS
, .TSX
). It means that all your new code can now leverage types:
Enjoy ?
Typing information for existing libraries
Have you been wondering what about the external libraries you are already using? How to get types for them? I was thinking exactly about this before migration.
It turns out that after you migrate JavaScript to TypeScript, most of the npm
packages you use already have built-in typing information. You can check it on your package’s npm
page:
If it doesn’t, you can try installing npm
package called „@types/your_library_name”, e.g. npm i @types/backbone
.
As a last resort, you can create the declaration files yourself.
JavaScript-TypeScript coexistence
A huge benefit of TypeScript is that JavaScript code can co-exist with it. If you are like we were a year ago, with a lot of legacy JavaScript code and are afraid of switching to TS – don’t be. TypeScript makes it very easy to maintain your JS codebase or even mix it altogether.
There are no issues in using JavaScript code in TypeScript and otherwise. You can even use JS components in TypeScript. We used to have a services.js
file where we kept „services” exported as follows:
After migrating it to TypeScript (meaning renaming to services.ts
) it turns out that these two definitions can co-exist, even in the same file:
As you can see, we added typing information to the legacy StockService
as well. That’s because we have noImplicitAny
enabled in our TypeScript configuration. It doesn’t allow leaving any variables which are of any
type. In any case, if you don’t know what type to use for a given variable/function, you can always type it explicitly as any
, while moving the new code to fully-typed equivalents. That’s the power of TypeScript ?
Compile errors
I should have started with that! It might be obvious, but not for people who only worked with JavaScript on the frontend before ? Sure, you could get suggestions and warnings with ESLint, but that’s not the same as really enforced types checking on compilation level:
Many TS compiler options
TypeScript compiler has many useful configuration options. If you want to support JavaScript alongside TypeScript, you should enable allowJs
. We have already mentioned the noImplicitAny
setting that forces you to always explicitly specify types in your code. There’s much more stuff helpful in various scenarios of JS-TS migration. We were having some issues with imports from modules without default exports – enabling allowSyntheticDefaultImports
saved us there.
What I’d like to emphasize here is that TypeScript compiler is really helpful ? It allows you to set your „typing” level by enforcing less or more rules. Thanks to that, you can slowly move towards more strict type checking rules like noImplicitAny
. You’re not forced to do that from the beginning.
Better coding experience
This is definitely the best thing about TypeScript. It just feels more right to use it than coding in plain JavaScript. I see no reason to start a new project with vanilla JS today. I’d say even more – I see no reason to not migrate your JavaScript app to TypeScript. The sooner, the better!
You can catch your errors earlier. No need to debug the code to see if the property you used actually exists on an object. Your IDE gives you code completion suggestions. It’s a pure pleasure ?
Migration challenges
Of course, we met some challenges during migration. I don’t call them issues, because they are all manageable. Many people have already done those things, so you can find solutions to almost any issue online. Let’s see what we had to overcome.
webpack configuration
If there’s something I don’t like about web development, it’s definitely webpack... This is a typical „fighting with machines” case. Poor documentation, lack of well-described solutions online and some specificity in every project.
We had problems with babel-loader
which we used to transpile JavaScript files before switching to TypeScript. Initially, afraid of introducing a regression, we wanted to keep babel-loader
for legacy JS files. We couldn’t make it work, so we decided to switch completely to ts-loader
for all files (including JavaScript ones).
Next issue was a lack of proper source maps and problems in debug mode. The breakpoints were often not hit. After a bit of searching and trial-error sessions, we found a solution. It was necessary to modify webpack.config.js
and set devtool
webpack setting to eval-source-map
. We also had to use source-map-loader for JavaScript files. Finally, this webpack configuration solved the debugging problems:
You need to be really patient with webpack… But if you have already worked with it, I guess you know that very well ?
Adding types to legacy JS code
Adding types information with noImplicitAny
set to true
is sometimes challenging. Especially if you have some bigger JS files with no typing information at all. Sometimes you need to search for function’s calls to find out of what types are its parameters. In other cases, you might need to run your app and see that at runtime ?
Fortunately, as we already said, TypeScript doesn’t force you doing that. You can play with tsconfig or use @ts-ignore as a last resort.
Handling libraries without types
Some of the libraries we used don’t provide any typing information. It makes working with them a bit difficult, especially if your TS compiler is already configured in a bit stricter mode. In such case, you might get an error similar to the following one:
Your options here are to either write the declaration files yourself, or import the library differently. You can, for instance, require
it:
const FormIO = require('react-formio');
and then use this FormIO
variable which is typed as any
. Again – that works because of TypeScript’s flexibility, but might not be very comfortable to live with.
Migrate JavaScript to TypeScript – should I do it?
Of course! There’s only one right answer. Don’t wait too long – start being more productive with TypeScript as soon as you can ? Believe me – you will not regret it.