Migrating a 20K SLOC production project to TypeScript

March 28, 2021 JavaScript TypeScript Sintra

I’ve migrated Sintra from JavaScript to TypeScript. There are 20K SLOC in the project. It took me a couple of weekends to finish. I’m incredibly happy with the result.

Why

Why, indeed! I don’t think “an increase in productivity” does it justice. TypeScript gives you incredible creative freedom.

In JavaScript, you never trust your code to be correct. You can only trust what you can read and comprehend, or what is covered by unit tests. So you tend to program defensively, and avoid large refactoring, because it’s never clear what would break. With time, adding features becomes more and more complex.

TypeScript will verify type cohesion of your entire project, as you type (pardon the pun). Pass a string argument instead of a number and it will warn you. Or return an undefined where an object is expected. Or use a React component with invalid props - TypeScript typings make proptypes look like a child’s toy. A whole class of errors disappears - you don’t have to write tests for them, you don’t have to debug them.

But best of all, you don’t need to think about typing errors when writing or editing code.

And besides that, you get fantastic IDE features. Full code completion. Automatic imports. Symbol and file renames, with auto-updating references.

What to expect

TypeScript is designed to be added to existing projects smoothly and gradually.

Any valid JavaScript is also valid TypeScript. So all your code will continue to work without modification. Also, if some team members only know JavaScript, they can continue to write JavaScript with minimal impediments. TypeScript can still infer types for JavaScript code and type check it (with limited success).

TypeScript does no modifications to your code runtime. All of TypeScript’s type checks work in compile time. This means that your code will continue to work even if the types don’t compute - useful when you’re hacking on a solution, or when you can’t get the types just right. On the flip side, though, validating TS types at runtime is not possible - and you ought to check types manually when reading untrustworthy external sources.

All libraries have TypeScript typings, by now. They are either built in are provided in a sidecar @types package. Only the most niche projects don’t. And you can always write your own declarations for packages, in the form of .d.ts files - TypeScript designers planned for this, too.

The compiler

There are actually three tools for compiling TypeScript that I use, in different use cases:

All of these use the same configuration files, so the difference is only in convenience.

The linter

ESLint is indispensable as is, and it has great TypeScript support with TypeScript-eslint. If you don’t use it yet, I suggest first setting up ESLint for your JavaScript project, and fixing issues, before you start the switch to TypeScript.

Prettier supports TypeScript and will greatly help with formatting while you are adding all those type annotations. Again, if you don’t use it yet, I suggest migrating to Prettier formatting first.

Doing the migration

All right, so you have decided to migrate, and you have cleared a couple of days of your schedule.

Firstly, you want to rename all your .js files into .ts. credit to this gist, here is a shell one-liner to do so:

find src -name "*.js" -exec sh -c 'mv "$0" "${0%.js}.ts"' {} \;

Immediately commit the result so git tracks renames correctly.

Now, add all the tools and follow setup instructions: the TypeScript package itself, the Babel preset, ESLint integration. Create a tsconfig.json file, enumerate your source files there.

Confirm that your project continues to compile and run as it did before. As I said, complete or correct typings are not necessary for this. So you can ensure your toolchain works straight away.

Add a linter script to check TypeScript scripts:

tsc --noEmit

This will load your project, resolve all types, and output errors, but not emit any transpiled JavaScript.

Run the linter. It’s going to complain - a lot. Your next goal is to reach 0 errors, so 100% type correctness.

You can either power through (as I did), or cordon off parts of code (starting with your models), and introduce external types gradually.

Now, here’s the thing. Some JavaScript is very hard to type. For example, React HOCs are harder to type than hooks. Classic jQuery style wishy-washy parameters are hard to type. Self-dependent API constructs (like the one Rematch uses) are hard or impossible to type well.

Which means, you will inevitably encounter some code you will be tempted to rewrite. Don’t rewrite everything at once! At least not until you have proper types in all other places. It’s much simpler to start with explicitly forcing the types of some values or functions, and refactor later. This way you won’t introduce logical bugs.

Some useful beginner tips

Writing good types comes with practice. Try not to dig into advanced derived types straight away. Simple types might not be the best elegant, but they work.

Learn about utility types, they will help in many situations.

Learn about the @ts-expect-error annotation - the one to use when you know the types are messed up (as it sometimes happens with external libraries).

Enable noUncheckedIndexedAccess right away, it’s one of the best TypeScript features.

Enable no-explicit-any. Learn about unknown and never, the types to use instead of any.

Use lodash, it types great and helps with eating potentially undefined values and with runtime type checks.

But most of all, go forth and practice. It will only get better from here.

Buy me a coffee Liked the post? Treat me to a coffee