Kuatsu Logo
Back to the blog
January 1, 20225 minutes reading time

A Guide to TypeScript Migration

Foto eines MacBooks mit geöffnetem Code-Editor
For some time now, TypeScript has been our primary language, both for internal projects and client projects. From the beginning, we have worked mainly with JavaScript (in combination with some frameworks like React and NodeJS), as the flexibility and wide application range of the language allow us to use a single codebase for an entire project, including web and mobile app as well as backend, in the same language. Not long ago, we migrated all our current projects to TypeScript.
Even though every JavaScript project is technically a valid TypeScript project, as TypeScript is a superset of JavaScript, not every JavaScript project is automatically a good TypeScript project. Adding TypeScript to an existing JavaScript codebase will result in all variables, constants, and objects not derived from the immediate context for the compiler being considered of type 'any'. Clearly, this is not the intended use. TypeScript was developed with the intent to use strict types in JavaScript and largely avoid dynamic typing. As a developer, you are therefore forced to manually type many of these objects. It's logical to think twice about whether this is truly necessary and if the benefits outweigh the costs or time investment.
There are now numerous tools and programs, like "ts-migrate" by Airbnb (which also uses TypeScript as their official frontend language!), which aim to simplify TypeScript migration. However, these are by no means perfect and do not eliminate the need for manual object typing. Computers are ultimately not very good at understanding the semantics of your code (although this is rapidly changing with the advent of deep learning and AI technologies like GitHub Copilot). So, we are faced with a dilemma: Migrate – or continue managing the old codebases?

Why TypeScript is simply better

Before tackling the question of whether migrating existing JavaScript projects is worthwhile, let's first examine the two biggest advantages of TypeScript.
  • The TypeScript Compiler (TSC). In our opinion, the biggest advantage of using TypeScript lies in the compiler. JavaScript is usually an interpreted scripting language, interpreted and executed by the browser at runtime. Compilation is therefore unnecessary. TypeScript is transcompiled to JavaScript (which allows the browser to interpret the compiled code as regular JavaScript). The advantage here is mainly that TypeScript code can also be transcompiled to older JavaScript/ ECMAScript versions. So, you can use modern features like Promises even when building your applications for older browsers or environments. TypeScript handles compiling the features into equivalent structures of the targeted JavaScript version.
  • Error detection at build time. Naturally, interpreted, and not compiled languages, also offer advantages like faster development cycles, but they also have a significant downside. Many errors that a compiler could usually catch are only discovered at runtime. TypeScript can help here in the largest category, type errors. Every JavaScript developer has at some point accidentally tried to access attributes of undefined or null. TypeScript can prevent such errors before the application reaches the staging or even production environment.

Why and how we migrated all (current) projects

There are plenty of reasons for TypeScript migration. But is it really worth migrating even existing, sometimes huge codebases? The answer we gave ourselves to this question was a clear Yes. After having used TypeScript for some new projects and enjoying static typing in JavaScript, the decision was easy. The implementation is another story. Many of our ongoing client projects in JavaScript were several tens of thousands of lines long – so where to start?

Determine the status quo.

If you haven't previously worked with TypeScript "alternatives" like JSDoc, the likelihood is high that you have little understanding of which function actually uses which types. Therefore, it is wise to first create an overview of the current status. Which interfaces does the application talk to? What data is read and written? Is the source of this data type-safe? Comprehensive documentation of the code pays off at this point.

Enable the "Strict" Mode.

In general, TypeScript makes real sense in our opinion only with the use of "Strict" mode. Without activating it, TypeScript allows much more leeway, for example by allowing implicit 'any'. However, this is precisely what the migration is intended to prevent. We want to prohibit dynamic and non-static types, and by not using Strict Mode, we're digging ourselves into a hole.
When we first migrated projects to TypeScript, we initially migrated our projects in the "Non-Strict" mode. Only afterwards did we activate Strict Mode and weed out the remaining errors. However, we quickly found out this was not the most effective method. Activate Strict Mode right at the beginning of the migration. You will save yourself a lot of headaches later on. Bug fixes and optimizations made in the first step often need to be entirely discarded after activating Strict Mode. Even if the error tab in your IDE might intimidate you: Use Strict Mode from the get-go.

Install declaration files.

A large portion of the errors shown by TypeScript might not actually come from your code, but from the node_modules folder. This is simply because most modules do not include type declarations by default. Fortunately, the open-source community offers suitable type declarations for almost every module. For a module "some_module", you can usually install these with $ npm install @types/some_module. However, if your application has a very specific use case and therefore uses very specific libraries, it may happen that no matching type declarations are available. Instead of simply declaring these libraries as 'any', invest the time to create type declarations. Tech debt is a real issue – and you want to avoid getting entangled in it at this stage.

Migrate classes and functions.

Before moving to the finer details, it's a good idea to first type the used classes, functions, and larger objects of your application. It's likely that these are used repeatedly in the code. Therefore, they serve as the first candidate for typing because the typing of other variables and objects will become much easier later on. During our migration, we initially tackled aspects like middleware, models, etc., assigning them the most precise types possible, which made it much simpler to assign and type them later in the smaller functions and sections of the code.
Ensure your typing approach is as strict as necessary, but not too strict: TypeScript is a very powerful tool, and you can make your types as stringent as you like. However, "don’t over-engineer"! Overdoing it with types makes little sense from a cost-time perspective beyond a certain point.

Other stuff…

A significant portion of the work is now done in a relatively short time. But now it's down to the details. Experience shows that it's often sufficient to give variables and co. a predefined type like "string". The well-known Don’t Over-Engineer principle applies here too.
You should also adapt your tests to TypeScript. Many popular test frameworks are optimized for TypeScript and work seamlessly with it.

Conclusion

TypeScript is possibly the best thing that has happened to JavaScript in recent years. The trend, according to recent StackOverflow Surveys, is increasingly towards statically typed languages. The flexibility of JavaScript combined with secure typing makes every project future-proof and easy to manage. Since our full TypeScript migration, it has become our de-facto standard, and we wouldn't want to miss it.