How not to trick TypeScript compiler and not be tricked by it
I'll give an advice on how approach typing in TS and show tricky examples using TS where wrong assumptions might lead your to errors. We'll also explore the connections between types along the way.

How not to trick TypeScript compiler and not be tricked by it
I'll give an advice on how approach typing in TS and show tricky examples using TS where wrong assumptions might lead your to errors. We'll also explore the connections between types along the way.


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 typesLink to this section
Consider this simple example:
<>Copyinterface Company { name: string; } class Vehicle { manufacturer: Company = {} as Company; name: string = ''; } class Car extends Vehicle { engineType: 'Electric' | 'Internal Combustion' | 'Hybrid' = 'Internal Combustion'; } class Bicycle extends Vehicle { numberOFWheels: 1 | 2 | 3 | 4 = 2; }
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:
<>Copyfunction appendToVehicles(vehicles: Vehicle[], vehicle: Vehicle) { vehicles.push(vehicle); }
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
<>Copyconst cars: Car[] = []; appendToVehicles(cars, new Car()); appendToVehicles(cars, new Bicycle()); console.log(cars);
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:


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:
<>Copyfunction appendToVehicles<T extends Vehicle>(vehicles: T[], vehicle: T) { vehicles.push(vehicle); }
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:


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:
<>Copyclass Bicycle extends Vehicle { numberOfWheels: number = 2; } function getAllByciclesByNumberOfWheels(bicycles: Bicycle[], numberOFWheels: number) { return bicycles.filter(bicycle => bicycle.numberOfWheels === numberOFWheels); } const bicycles: Bicycle[] = [new Bicycle(), new Bicycle()]; console.log(getAllByciclesByNumberOfWheels(bicycles, 2));
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?
<>Copyclass Bicycle extends Vehicle { numberOfWheels: 1 | 2 | 3 | 4 = 2; } function getAllByciclesByNumberOfWheels(bicycles: Bicycle[], numberOFWheels: number) { return bicycles.filter(bicycle => bicycle.numberOfWheels === numberOFWheels); } const bicycles: Bicycle[] = [new Bicycle(), new Bicycle()]; console.log(getAllByciclesByNumberOfWheels(bicycles, 5));
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:
<>Copyfunction getAllByciclesByNumberOfWheels(bicycles: Bicycle[], numberOFWheels: Bicycle['numberOfWheels']) { return bicycles.filter(bicycle => bicycle.numberOfWheels === numberOFWheels); }
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 typesLink to this section
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:
<>Copyfunction deepCopy(obj: object): object { // some heavy lifting }
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:
<>Copyconst car: Car = deepCopy(new Bicycle()) as Car;
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:
<>Copyfunction deepCopy<T extends object>(obj: T): T { // some heavy lifting }
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 the compiler will catch for us
ConclusionLink to this section
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 one.
Comments (0)
Be the first to leave a comment
About the author

Senior Angular developer from Armenia. Passionate about WebDev, Football, Chess and Music

About the author
Armen Vardanyan
Senior Angular developer from Armenia. Passionate about WebDev, Football, Chess and Music
About the author

Senior Angular developer from Armenia. Passionate about WebDev, Football, Chess and Music