How not to trick TypeScript compiler and not be tricked by it

Armen Vardanyan
Angular In Depth
Published in
6 min readJan 25, 2020

AngularInDepth is moving away from Medium. This article, its updates and more recent articles are hosted on the new platform inDepth.dev

If you work in web development, there is almost no chance you have not ever heard about TypeScript — the strictly typed superset of JavaScript. Chances are you even (willingly or not) learned at least the basics of this language. Most of the major frameworks like Angular, React and Vue offer you the ability to write your projects in TS rather than plain old JS. But Angular was the first one to massively adopt it, so, for Angular developers, knowing TS as well as possible is a must.

I won’t be listing the benefits of using TS, or the basics of the language. Instead, I am going to focus on some patterns that often mislead developers writing code in TS, and help everyone utilize the power of static typing better.

Understanding the connections between types

Consider this simple example:

Basic inheritance

Here we have a base class Vehicle, and two subclasses, Car and Bicycle. They don’t have much properties, so it’s easy to grasp their meaning and purpose.

Now imagine we want to write a function that will take a Car or a Bicycle, and append it to an array of Cars or Bicycles. Because they both extend from Vehicle, we need a function that takes an array of Vehicles and a Vehicle and appends it to the former:

So far so good. Our function is strictly typed, so we cannot push a number, for example, into the array of Vehicles. But there still a pitfall. Do you see it? If not, take a look at this example usage of our function:

Here, we take Bicycle and try to push it into an array of Car instances. “No way this gonna work!” you’re probably thinking now.

Spoiler: way!

The code compiles successfully, TS compiler does not throw any warnings whatsoever, and if we open the console we will see the following:

How is this even possible?!

So, now we have an array of Cars that has a Bicycle in it. But what went wrong? Did TS fail us? Should we go open an issue on the github repo?

No, no. In reality, what happened is that we failed to tell TS exactly what we want. We told TS to create a function which accepts an array of Vehicles an another Vehicle, and then append the latter to the former. Which TS did masterfully — I can only pass an array of Vehicles and a Vehicle to it, and nothing more. Which I did — both Cars and Bicycles are, in fact, Vehicles. But what I really wanted TS to do is to create a function that accepts an array of Vehicles, and a Vehicle of the exact same type as that array. Here is where the problem hides! Now look at this code:

Now we tell TS the following: “we have a function that accepts a Vehicle and an Array of Vehicles of the same type”. If we put this code in the TypeScript Playground, we will see the following:

Fixed!

Now it works exactly as intended. Because the array that we passed to the function is an array of Cars, TS inferred the generic type T to be Car , and thus the second parameter can only be a Car.

This brings us to our first piece of advice:

On the contrary to tight coupling between functions/classes, tight coupling between types is a good thing

So understanding the connection between the type of the first parameter of the function and the second helped us to come up with a better type guard. Now, let’s take a look at the next example:

This is the same Bicycle as in the previous example, except that the numberOfWheels property is a number yet, not the more strict 1 | 2 | 3 | 4. Then we have a function that takes an array of Bicycles and filters by number of wheels. So, because the numberOfWheels property is a number, then the corresponding function parameter is also a number. But what if we come up and change the type of the numberOfWheels property to the more strict (and more reasonable) 1 | 2 | 3 | 4 again, as in the previous example?

Notice how we changed the numberOfWheels property on the class, but not the corresponding function parameter. This actually resulted in an overlooked type problem: by accident I typed 5 instead of 4 when calling the function, and now it will always return an empty array, because there are no Bicycles with 5 wheels (well, at least not in our implementation). This means that we have to change every reference to this type every time we change the typing. Or does it?

In reality, we can tell the TS compiler “this parameter will always be of the same type as some property of some class”. Here is how:

Now this way we tell the compiler that the numberOfWheels parameter is going to be of the same type as the numberOfWheels parameter on class Bicycle. If the latter is number , it will be of type number, if it is a string, it will be a string, and so on, always the same. If we call our function again with the same wrong parameter, we will see an error:

This brings forth the second piece of advice:

We should look for single source of truth for types whenever possible, as we can overlook that some types are, in fact, entirely dependant on types of other properties or variables

The flow of types

Sometimes the type of the parameter that enters a function will determine the output of it. Consider this function, that creates a deep copy of any object it gets as a parameter:

This receives an object and returns an object, obviously. From the first example in this article, you may start feeling that something is not right here; how would TS know that the returned object has the same interface as the entering parameter (it has to, because this is a deep copy)? So mistakes like this are possible:

TS allowed as to “safely” cast the resulting copied object to type Car even if in reality this is an instance of class Bicycle. Of course, from the same first example you may know that this can be fixed using a generic type:

Now the wrong type cast will not be allowed by TS:

Which brings us to the third piece of advice:

We should not be very sure of the mistakes that th compiler will catch for us

Conclusion

This is in no way a definitive guide to writing in TS; but it will help reevaluate how we perceive the type safety of our codebases. The TS compiler is very powerful, but even it can be tricked, so caution is always the advice number 1.

--

--