For the past year I’ve been working on a JavaScript project for my client. We have a mid-size web application. We use React as the web development framework. Few months ago we decided to start migrating to TypeScript.

Today I can say that it was the best decision we could make. TypeScript makes working with JavaScript, which is sometimes surprising and quite hard to understand, so much better experience. However, there were some challenges on this journey. If you want to know what issues we met and how we solved them, but also what huge advantages TypeScript gave us – read on πŸ˜‰

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

Before starting migrating to TypeScript, we had to choose the best strategy for this process. There were two possibilities we considered: migrating the whole JavaScript codebase to TypeScript at once or switching to TypeScript compiler and migrating part-by-part.

For those who are not familiar with TypeScript – it is a superset of JavaScript which adds typing information to your JS code. It means that every JavaScript code is also a valid TypeScript code. When compiling TypeScript code, TS compiler removes the typing data and outputs readable, vanilla JavaScript code ready to be executed.

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

Because of that, we decided to migrate part-by-part, having JavaScript and TypeScript files at the same time. It is possible thanks to enabling allowJs TS compiler option. It means that TypeScript will compile both TS and JS files. Such approach allows the coexistence of old JS code with the new TS parts. It also allows moving only chosen parts of the application to TypeScript at the beginning and come back to the others later.

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

If you are a JavaScript developer, the best advice I can give you is to start using TypeScript πŸ™‚ In this section I’m describing what positively surprised me since the beginning of starting to use 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? πŸ˜‰

JS-TS friendship

Coexistence and friendship between JavaScript and TypeScript is HUGE. These two creatures are not enemies – they are best friends 😎 We’ve already gone through many scenarios when we needed to use something written in TS in a JS file and otherwise. It turns out that all of these scenarios are manageable. Sometimes you need to import something differently (for example using require instead of import), but it’s all possible. We found it super easy to write new features or even add parts of new features to already-existing JS code, writing everything in TypeScript.

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:

module.exports.UsersService = function (url) {
// … UsersService init code …
};
module.exports.UsersService.prototype.getUser = function (id) {
// … code to get active user by id …
};
export class NewUsersService {
url: string;
constructor(url: string) {
// … NewUsersService init code …
}
getUser(id: number): JQuery.Promise<UserViewModel, any, any> {
// … code to get active user by id …
}
}
view raw services.ts hosted with ❤ by GitHub

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.

This is somehow the JavaScript you-can-do-whatever-you-want philosophy smuggled into TypeScript 😁 But in a much smarter way. I’m sure that when you start working with TypeScript you’ll get what I mean by its wisdom πŸ™‚

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

When switching to TypeScript we had few issues with webpack configuration. We have used babel-loader before for transpiling JavaScript and initially we wanted to keep it in the TS compilation process. We first tried to keep babel-loader for JS(X) files and use ts-loader for TS(X) files, but it didn’t work – we were getting weird compilation errors.

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.

Finally, we decided to compile everything – both JavaScript and TypeScript files – using ts-loader (including React files). It worked, except source code maps. When we tried to debug in Chrome console it seemed the source code is not refreshing properly after rebuilding. Breakpoints were often not hit and the debugging didn’t seem right.

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:

devtool: "eval-source-map",
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
exclude: /node_modules/,
loader: "source-map-loader"
},
{
test: /\.(t|j)sx?$/,
exclude: /node_modules/,
use: {
loader: 'ts-loader'
}
},
],
},

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

I found out that JS to TS refactoring is not always an easy task. Some things that are allowed by JavaScript are not valid in TypeScript. In most cases it means that something was wrong with your JavaScript code, but we all know how it is with legacy code refactoring πŸ˜‰

As an example, we had several functions that we added to a String prototype in JavaScript:

String.prototype.contains = function (it) {
return this.indexOf(it) != 1;
};

Such code becomes invalid in TypeScript:

Migrating to TypeScript: TS error about non-existing function on String prototype in VS Code
TypeScript complaining about non-existing function

A non-obvious solution to that issue is to extend String interface by declaring typed functions you want to add:

interface String {
contains(this : string, it : any): boolean;
}
String.prototype.contains = function (it) {
return this.indexOf(it) != 1;
};

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 😎

Here comes the problem. First of all our C# data models which we returned from the controllers were not the best. In most cases we returned a DTO object which contained many properties from which only few were used by JavaScript code. We never saw this problem, because in JS we just dynamically typed properties names we wanted to use. We didn’t see how much of not needed data is sent from ASP controllers to the web browser.

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

Migrating to TypeScript has been a very interesting and rewarding process. Compared to vanilla JavaScript, TypeScript lets programmers see many problems. This allows to see how bad our codebase was. We even realized that our server-side C# code was not the best (see the section about view models above). Thanks to introducing TypeScript we improved a lot of our JavaScript and C# code, including its architecture.

I think that TypeScript uses many simple concepts under the hood and it makes a lot of things possible (see keyof or unknown). In many cases TypeScript lets you do something, but doesn’t force you to do it. That’s the beauty of this typed JavaScript world πŸ˜‰