How TypeScript is making programming better — Tips and tricks after migrating to TS
Ably is a realtime platform that can be used to easily plug in realtime functionality into your application. Very recently, we migrated our main project from JavaScript to TypeScript. Here’s a summary of why we did what we did, and how.
I shall divide this article into two sections: The first where I zoom through the basics of TypeScript and cover only the must-know parts in order for you to get started quickly. In the second part, I’ll focus on how Ably evaluated the use of TypeScript as a solution to a few challenges as well as the exact steps we followed for easy and stable migration.
Section I
What is TypeScript?
If you are a web developer, I’m sure TypeScript has created enough buzz by now for you to know about it. However, for some of us geeks living under a rock while coding away most part of the day, here’s a quick intro:
JavaScript, as we all know, was originally meant for building logic into small front end apps in order to make them function dynamically based on user interaction. But soon, we realised it was so useful, diverse and easy-to-use that we went on to use it for everything, even for very large projects, for both frontend and backend applications, etc.
But JavaScript wasn’t built for this kind of use and as you’d expect, it soon started to pose problems within complex projects. Developers found themselves spending much more time on debugging and maintaining the code rather than writing new code.
What now?
The solution was to either uses a more strongly typed language or turn JavaScript itself into one such language. Of course, anyone having spent years building a project in JavaScript would be tempted to go with the second option if it was possible.
Then came TypeScript, donning the hat of a saviour god for all the devotees in JavaScript land!
By definition, TypeScript is a typed superset of JavaScript. This means that all the existing JavaScript is also valid TypeScript. You can convert an existing JavaScript project to contain type definitions and other language features that make the maintenance and scalability of your project much easier and efficient in the long run.
TypeScript comes with a compiler and while a .ts file cannot be run in a browser when compiled, it gives back a .js file which is plain JavaScript as if we originally wrote the whole project in JavaScript itself. While including such a ‘convenience layer’ over JavaScript code isn’t hard and doesn’t hurt any existing infrastructure at all, having such a layer serves to be hugely beneficial as you’ll see upon reading further down this blog post.
Key Concepts in TypeScript
Here’s what makes TypeScript so useful:
Types
Types, as you’d imagine, form the very crux of TypeScript. It has the following basic types: Boolean, Number, String, Array, Enum, Any, Tuple, Never, Void, Null and Undefined. Most of these are straight forward as you’d expect, but you can check out the detailed description here.
Interfaces
TypeScript supports duck-typing and structural typing via Interfaces. You can declare and use interfaces as follows:
Using an Interface, you can specify a structure for the types to be used as parameters to the function. Duck typing allows this function to accept any parameters that fall within the same structure i.e, both of them being numbers in this case. It’s no longer necessary for objects of this type to be instances of the Interface in question only. One thing to bear in mind is that Interfaces completely disappear when the code is transpiled to JavaScript. Hence, make sure your optional variables are not being used within the implementation.
Classes and OOP
Classes and other object-oriented features are already part of ES6 but not all browsers support these yet. Using classes within TypeScript with the correct target version gives a way to start using these features in your code without having to worry about browser compatibility.
Section II
Migrating to TypeScript
Motivation
The Ably project started a few years back and due to its initial focus on R&D rather than sales or funding, the project grew at a very fast pace and before anyone could realise it, it soon contained thousands of lines of code. That’s when a discussion sparked up within the core tech team with a proposal to start using a strongly typed languages like TypeScript or Flow.
There have been many discussions, talks and articles generally on the web that compare Flow vs. TypeScript. Here are a few examples: “Type systems for JavaScript” by Oliver Zeigermann, “Fast and precise type checking for JavaScript” shared by Adrian Coyler, etc. A quick run-through of such articles makes it easy to decide which one’s best for your project.
In our case, after a lot of research, we found that TypeScript won the war due to it’s stability, huge community support, overall better tooling as well as better errors that point out what’s most likely wrong in your code, among other things.
Approach
The first step was to set up a TypeScript configuration that would build all of our existing Javascript and any new TypeScript source files into a brand new directory (using the — allowJS compiler flag).
Once the project was configured to allow TypeScript code, we began by converting the files that are used most often and have the most active development. This made converting other files progressively easier since the dependencies would already have their types declared.
Thereafter, we followed the process below for converting each file:
- Change the file extension from .js to .ts
- Convert common.js imports/exports to ES6 style
- Compile the file and add types to fix any warnings generated (it’s useful to have editor extensions for this)
- Declare basic types for any dependencies that do not have types available.
- Convert ES3-style function classes into Typescript classes
- Add types to all functions and method signatures, as well as declare interfaces or type aliases where useful
- Ensure unit, module and integration tests still pass.
- Open another file and repeat the process
It’s worth noting that TypeScript classes cannot extend plain (ES3-style) JavaScript classes without TypeScript declarations. So if many classes extend a common base class, it becomes necessary to convert or find declarations for those first.
Our aim was not to convert the entire codebase in this manner, but rather to convert enough that any ongoing work could be now done in TypeScript with minimal friction.
What’s better after?
As mentioned in the first part of this post, after migrating to TypeScript, it’s not only easier to catch any errors caused mainly due to bad typing, it also makes maintenance and especially further refactoring of code extremely easy. Also, since TypeScript includes enough information about types within the code itself, you can completely do away with JSdocs, and new programmers in the team find it very easy to onboard themselves with the project quickly by just reading through the code.
Tips & Tricks — What no one told you about TypeScript
- noImplicitAny: Not annotating function parameters and class methods will lead TypeScript to implicitly consider their type as any. This defeats the very purpose of using TypeScript for type checking as these places are mostly prone to type errors. To avoid this, you can set the noImplicityAny flag in tsconfig file which forces you to annotate these, even if that means you explicitly specify any as a type.
- Object vs Any: If you don’t know which parameters a type will have, prefer the
object
type or{ [key: string]: any }
toany.
- Wherever possible, use object type instead of any, since it is more restrictive in terms of the existence of certain constructs.
- Function Overloads: Use function overloads where multiple call signatures are expected. This improves safety, readability and type inference and provides better editor suggestions.
- Function and class return types: Consider not always adding return types to short functions if it’s obvious. These can generally be inferred by the compiler implicitly while adding them would add unnecessary visual noise.
- Const variables rarely require type declarations: For some variables such as
const MAX_NUM: number = 1000
skipping the type would cause no harm in terms of understanding the variable type as it is pretty obvious, so it would rather make sense to declutter the code by skipping this info. - Make use of interfaces where possible vs. classes: As mentioned above, interfaces allow duck typing, they give you much more flexibility in terms of how you would like to pass on function arguments while still ensuring correct types. This is not possible with classes.
Thanks to John Diamond(Distributed Systems Engineer at Ably) for sharing his valuable insights on migrating to TypeScript.