A TypeScript migration post-mortem
When I joined KissMy in October 2020 I knew that the biggest project coming up was to rewrite our biggest project to fit the new backend specs we were migrating to. This meant rewriting almost all of our existing JS code (we are using Nuxt.js) to handle the new logic. Let’s join me for a TypeScript migration post-mortem tale…
Since the codebase was huge and we wanted to make sure the new version was also more stable and more maintainable than the v1, I quickly suggested that we took the opportunity of a rewrite to migrate the entire codebase to TypeScript. I come from an Angular background so I already knew about the advantages of a statically typed JS, and my team was also easy to convince, so this is how we did it, what went wrong, and how we keep on improving things now that the v2 is in production.
What is TypeScript ?
DefinitelyTyped repository on Github.
TS is great because one of the things thats the most frustrating when developing large JS applications is the dynamic nature of JS types. A variable can be any type (including
undefined at any time in you app’s lifecycle). And this sucks. Just look at the number of results on Google for one of the most common errors in JS.
And the larger you app gets, the greater the chances are of something like this happening “randomly” in production. TS helps you prevent this since is raises errors at build time and even in your IDE.
One of the good things about TS is that it is a superset of JS. It means that any valid JS code is also valid TS code. This is great since it technically allows to adopt TypeScript incrementally in a project, making TS files live alongside JS files in a same codebase, and migrating one file at a time.
As you’ll see though, this is not the way we decided to do things. In our case, we already had a pretty big JS codebase that was going to need an entire overhaul anyway, mainly to fit new entities coming from our backend, but with minimal friction regarding existing logic and features.
What we did
We spent a lot of time thinking about how we could achieve this, and we decided that the right thing to do was following these steps:
- Setup our existing Nuxt.js app to use TS, this was pretty straightforward since the docs are very nice.
- Create some TS files defining static typings for all the entities we were going to manipulate coming from the backend (Customers, Bookings etc…)
- Change every single JS file in our app to a TyeScript file using our newly created types and fix the resulting errors (this was painful, especially our Vuex store files)
- Migrate every single Vue SFC from JS to TS and fix the resulting errors (this was also painful)
- Profit (Maybe?)
As I said, it was painful, there’s nothing fun in seeing your app break over and over again. I’ve mostly been working on this alone, pretty much full time, for the better part of an entire month. It was frustrating and sometimes a bit overwhelming. But once I was done it felt great !
Why did it felt so good ?
Well as I said, the biggest strength of TS is that it leverages it’s type system to raise errors about your code directly in your IDE. Let’s look at the following snippet.
Notice the red squigly lines under
user.name? My IDE is telling me that this is possibly undefined, and might throw a nasty error at runtime (and sometimes you won’t catch that when developing, and the error might happen in production). Now on the next screenshot you can see that I added an
if statement, cheking for the existence of
user.name and the red squiglies are gone ! And this error will not happen at runtime.
Now this is a simple example but imagine if (like us) you had hundreds of files, entities, components etc… all interacting with each other, sometimes through a Vuex store. Having nicely defined types helps you prevent dozen of runtime errors in production.
This also is a huge win when it comes to development speed. Having tools you can actually trust is one of the greatest thing that can happen to a developer. I don’t have the exact metrics, but I can safely assume that our development speed (and its reliability) has greatly improved since moving to TS. The fact that our IDE and build chain throws errors at us during development allows us to make changes to the code base with much more confidence.
What we missed
Obviously we’re only humans (though I have my doubts about some of my teammates) and we make mistakes. One of the thing we don’t lack at Kissmy is optimism. Sometimes it’s a good thing, sometimes it’s not…
In the case of migrating this project to TypeScript we failed at anticipating multiple things. I think the biggest one is that we greatly underestimated the importance of well defined, nicely abstracted and easily maintainable types. Its easy to sometimes get a bit lazy when it comes to create your typings (like adding an
any type to a value because typing it properly would require to define some complicated types.
Turns out this is important. I recently had to build a new feature on this app, and even though TS helped me a lot a usual, I ran into a bit of an issue when I took the time to replace a type that had been lazily defined as
any with a well crafted discriminated Union Type. This started to break our build process left right and center since everywhere that type was referenced, my newly created type didn’t meet the TS compiler’s expectations.
This also emphasis an other issue we had with this migration process: our team’s lack of TS experience.
As I said, we’re optimist people, and when we made that decision, I was the only person in the team who had ever worked with TS, but I still was not a TypeScript wizard. The other two were very motivated about learning it, but it turns out that properly defining types for a big app with lots of dependencies and intricate entities is hard. This led to even more poorly defined types, and sometimes errors that we didn’t see coming because we did not properly define some variable as being nullable or optional.
The takeaway (not the food, the lesson)
So as I said in the intro, it’s been a year now since we started using TS on this big project of ours, and this is a tldr; of what we learned:
- USE TYPESCRIPT EVERYWHERE ! (seriously, it’s a game changer)
- Take the time to properly define your types, with nice abstractions, and the future in mind
- Properly learn TS. I know this sounds like a no brainer but I’m actually serious about this. TS has two sides. You can stick to basic types and interfaces, but that will lead to a ton of
anyor type casting in your code base, making TypeScript somewhat useless.
- DON’T TRY TO PLEASE THE COMPILER ! It’s here to help you, try to understand why it’s yelling at you rather than just trying to make that nasty error go away so you can commit and push faster. If making the error go away means refactoring your type definitions with something complex, DO IT, you will really see the benefits later on.
That’s it for me, thanks for reading this… Until next time !