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#
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:
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
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
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.
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
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
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!