Coming back

Hey, it’s me again! After a long break, we’ll talk about TypeScript and its type system, today.

We’re sorry it’s been so long since our last technical article on this blog! After the holidays, we have been very busy preparing an awesome product that we hope will help fight and erase abuse in the workplace, called PeerSpheres.

After many weeks working tirelessly on PeerSpheres, we are happy to say that we launched a campaign last week, to improve the awareness of abuse and other issues at work. You can find our GoFundMe campaign here and access the campaign’s main website at bethechange.io. We also launched a website where people that are victims of abuse at work can use in order to notify their administration or their human resources department of the problem, in hopes that they will act on it. If you or someone you know have been victims of such abuse, please do not hesitate to go to wewill.bethechange.io to send the word to your organization.

Avoiding mistyping your model in TypeScript

With that said, now that we’ve launched the campaign, we should be resuming our weekly technical article here on beslogic.com. We will continue publishing our articles on every Friday.

This week, I would like to look at the ways to avoid mistyping data in TypeScript.

Coming from C#

Most of PeerSpheres was previously written in ASP.NET, but we decided to switch to a single-page application framework, Angular, to improve the user experience and make it easier to develop frontend-centric features in the future. Doing so, we went from writing our frontend in C# to writing it in JavaScript, through TypeScript.

The language switch has mostly been a straightforward and pleasant experience, which is no surprise given that Anders Hejlsberg, the lead architect of C#, is also a core developer of TypeScript. This makes the language easy to take on, and the experience quite fun and seamless.

One key point where the languages differ, however, is an important one that can lead to buggy code that nonetheless compiles without warnings: it’s the fact that TypeScript will always type what it cannot infer as any. Using C# analogies, using any is like using dynamic: you lose all compile-time type checks, and everything goes. If you made a mistake in using the dynamic, you’ll learn that at runtime when you hit that faulty line:

Obviously, turning off static type checks must be done with caution and with good reasons. Most of the time, you can achieve the desired effect without using dynamics: this can be by creating an interface, using generic parameters or by doing pattern matching on the object’s type. Usually, we end up using dynamics only when dealing with deserialized data such as a JSON value, and we then make sure to properly type intermediary objects, to avoid runtime errors as much as we can.

Another difference is that you cannot write extensions to deal with nullables in TypeScript, as you could do in C# to help you deal with these values. If you missed it, I wrote an article about ways to empower nullables, some months ago!

any is not your friend

However, in TypeScript, there are many times when the data that we get is typed as any, and by not typing those values correctly, we can write incorrect code that will not be flagged by TypeScript.

Take, for instance, the Params that Angular’s ActivatedRoute uses. It has this index signature: [key: string]: any. What it means is that you can ask it anything, and TypeScript will not know what was actually returned (to be fair, TypeScript never knows what anything returns, because its type annotations are removed at compile time, and you possibly get pure JavaScript). This leads to errors in our code base, where we thought that params.id was of type number, when it was actually a type string and we needed to do a parseInt over it. Other times, we would ask for an optional parameter that was not provided, and would get a null or undefined value, without having warnings when referencing the value later on.

Thankfully, Angular eventually added a ParamMap instead, which is clear about its process: get(key: string): string | null. We then switched to using ParamMap exclusively:

Is it more tedious? I don’t think so: sure, you have to write more code, more checks, and maybe do some casts, but then you know your code will work at runtime, and is more impervious to code refactoring, as other programmers will get notified if they try to use your value incorrectly. I prefer to avoid runtime errors as much as I can, which can sometimes be hard to investigate.

How to describe data from the outer world

Another easy way to mistype data in TypeScript is to improperly describe data that comes from your backend. If you use C# as we do for your backend, you have to keep in mind that every reference type you return in your actions can be null (contrarily to value types, which have to be explicitly typed as nullable when desired). A common mistake can be to forget to match that in your corresponding frontend type, or to bypass typing what you received from the backend entirely.

The best way to catch this kind of typing mistakes is to have a thorough peer review, where someone else can look at your code with a fresh perspective. Other than that, you can always take the habit of typing every reference type as MyReferenceType | null in your models. It will not always be necessary, but that way, you avoid later runtime errors. Doing so, however, will potentially raise flags in large parts of your code. If having some fields null would make the object unusable, you can always use a Dto pattern with a separate constructor, that would return null if the object could not be created. This means that instead of dealing with separate fields being potentially null, you deal with a single, aggregate value:

Uninitialized properties are not defined

The last example I want to see with you is about properly typing values in classes.

When using interfaces as in the previous examples, you need to create the object yourself (or be provided with it from external code). As such, you cannot create an object without providing a value for every of its fields, enforcing consistency between the type declaration and the actual usage.

This is not true with classes.

When you define a class, you can also define its properties. Because the way JavaScript works, until these properties are initialized with an actual value, they will simply not exist on instances of that class (which in turn returns undefined when such a property is referenced). This means that TypeScript will believe that the properties are of the type you described in the class, even though they can absolutely be undefined until they are defined.

There are many ways to avoid this: you could use an interface, for the cases where you don’t really need to instantiate the object through a constructor; you could provide your properties with acceptable default values; you could make sure these properties are provided to the object via the class constructor; or you could simply type them as being possibly undefined. This will vary depending on your needs.

Letting TypeScript help further

Up until now, I have shown you some tips and techniques to avoid mistyping your TypeScript data, as a programmer. However, most of the time, it’s best if we can have some kind of tool to help us detect cases which we may not have noticed. By default, TypeScript is pretty lax about enforcing strong type safety, mostly because it wants to be a superset of JavaScript: most programs in JavaScript should compile in TypeScript also. It’s fine if you are porting an already existing JavaScript project to TypeScript, but if you are starting a project from scratch, I would encourage you to turn on the strictest rules in the TypeScript’s compiler options.

Most of the cases described above, as of TypeScript 2.8, have a strict option that allows the compiler to treat them as errors, forcing you to correct your code properly (or improperly, by casting everything as any, which TypeScript will let you do, but still shouldn’t do!).

You can either turn these checks at once, using the “strict” rule, or one by one, using the appropriate rule.

noImplicityAny will help you always have a properly typed model, strictNullChecks will force you to consider every case where a potentially null or undefined value is used and strictPropertyInitialization (added in TypeScript 2.7) will help you avoid the uninitialized properties having an undefined value, even when they are not typed as such.

I hope you will find these tricks useful! Remember that the type system is always your friend, and that making sure to have a properly typed program will help avoid runtime errors and make future maintenance easier.

That’s all for this week! See you next Friday!