I will not discuss TypeScript itself in this article. This is a pure recap of challenges and the experience I gained when migrating to TypeScript.
Choosing migration strategy
Migrating the whole codebase to TypeScript would mean to change all files extensions to TypeScript ones: .js to .ts and .jsx to .tsx. It would also mean that TypeScript compiler starts checking types in those files. This approach is doable, but – depending on the size of your project – can take a long time. During migration the code would be frozen until you fully migrate the project. Also, for TypeScript beginners, it could be a bit frustrating having to type everything at once or use some tricks to tell TypeScript to ignore checking types in many cases (because the type is not known at this stage, or we want to do it later etc.).
In our web application the biggest JS files have few thousand lines of code. Some of this legacy code still didn’t use React, but Backbone as their framework. We didn’t want to touch these oldest files in the beginning. Instead, we moved few recently written functionalities to TypeScript straightaway and decided to implement everything new in TS as well.
Bright side of migrating to TypeScript
Types “for free”
As soon as we changed our transpiler from babel-loader to ts-loader, I wanted to know how we can get typing information for already-installed npm packages. It turns out that for 98% of the packages we used there’s already a typing information available as a separate npm package.
In most cases if you have a npm package called abc there’s a typing package available called @types/abc. I explored our package.json and for each package listed there I just executed npm install @types/package-name. 2 or 3 older or not-so-popular packages didn’t have types definitions available, but for the rest we got types for free, without having to type anything ourselves. This was a first, very nice surprise 😉
You can find more information about TypeScript types definition packages, as well as contribute one yourself, here.
All new code is typed
As I wrote before, every new feature we’ve added since the migration is written in TypeScript. Thanks to that, all new code we write is strongly typed. What’s remarkable here is that it required almost no effort. We just switched the compiler to TS (with a small struggle with webpack configuration, about which you can read below) and suddenly got into typed world for every new feature we implement. Quite impressive, isn’t it? 😉
We also rewrote some JS files that were used from multiple places in the app to TypeScript. As an example of JS-TS coexistence, I’m presenting below the potential .ts source code you might have. It contains both old, legacy JS code when we used module.exports and prototype to expose various services for data fetching, as well as a brand-new, nicely-exported TypeScript class:
Such code can of course be in separate files, but it only shows how good TS and JS can coexist together. You can keep your old code working, while implementing anything new in TypeScript.
Flexibility in partial migrating to TypeScript
It turns out that choosing to partially move our web project to TypeScript was a great decision. The team wouldn’t be so happy and enthusiastic about TS if we told them to move legacy Backbone code to TypeScript. Instead, we gave them a nice tool which can be used for all new stuff. Also, if someone likes the idea and wants to move already-existing JS file to TS, he or she is welcome to do so.
That’s how we are still migrating our project to TypeScript, without any hustle and frustration 😉
Wisdom of TypeScript
I’m positively surprised by TypeScript almost every day. At some point I get to the issue that seems hard to be solved or quite non-standard. Then I find a solution online and it makes me really impressed about the job the community and Microsoft has been doing around the language.
These are sometimes small, but smart things. Recently we got surprised by keyof, which lets you get names of the allowed properties of your type. Generally the typing system is really advanced and impressive. For some it can be a disadvantage, but I think it gives an enormous flexibility.
Small effort, huge benefits
Last, but not least, I must admit that migrating to TypeScript was quite an easy and smooth process. The value for the effort is huge. You get a typed world with very little work.
Migrating to TypeScript struggles
During the migration process we met few difficulties. Some of them we managed to solve fully or partially. If you have any better ideas on these things, feel free to let me know in the comments.
Complexity of webpack configuration
Then we tried to compile everything using ts-loader and then recompile it again using babel to ensure (as we initially thought would be wise) backwards compatibility. We also couldn’t make it working.
After a bit of struggling we found a solution. We set devtool webpack option to eval-source-map and configured source-map-loader for .js files. Finally, the proper part of webpack config looks as follows:
So far we also haven’t noticed any issues related to backwards compatibility that were theoretically solved by babel. TypeScript compiler does its job very well.
Complicated JS -> TS refactoring
Such code becomes invalid in TypeScript:
A non-obvious solution to that issue is to extend String interface by declaring typed functions you want to add:
There are more subtle difficulties like that you can meet while migrating JS code to TypeScript. However, in the end it gives you a possibility to make your codebase better.
Keeping view models in sync
An issue I didn’t think about before migrating to TypeScript was a need to keep frontend and backend view models in sync.
One of the first things you’d like to have in your TypeScript codebase is to have as much typed data as possible. We use .NET and C# as the backend of our application. ASP.NET MVC controllers returned the strongly-typed objects into the previously chaotic and untyped JS world. Now we are strongly-typed with TypeScript, so we’d like all this data typed as well 😎
Now with TypeScript we needed to create these objects definitions also in TypeScript. Having a huge DTO object returned from the controller, with another X objects on which this DTO depends, it was a nightmare. It turned out that for a single DTO I had to create 10-15 TypeScript objects just to use 5-10 properties in the frontend code!
That’s the moment when we came to the necessity of creating web view models. The idea is to return a view model from a controller, not the whole DTO object. This view model contains only the data which is needed by TypeScript (frontend).
I think this is nothing new for experienced web developers. However, now comes the question: how do we keep backend (C#) view models in sync with frontend (TypeScript) ones?
Unfortunately, I haven’t found a perfect solution for that. The best method I came up with is… manually keeping these view models in sync.
There’s a C# to TypeScript VS Code extension which allows to paste the copied C# code as its TypeScript equivalent. It makes the job a bit easier. It even has a CLI tool, but it produces a single .ts file from a single .cs file. If you have multiple objects in a single C# file it will produce a single TypeScript file with all these objects. If any of those objects is used in other files (unfortunately we have many of such cases), these dependencies will not be automatically added as imports in the auto-generated .ts files.
However, I don’t find it very problematic for now. As soon as the TS view models are created, we just need to update them as often as we update C# view models. I think this is a very common issue so if you know any better solution here – please share in the comments 😉
Migrating to TypeScript – summary