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

Post Editor

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.

6 min read
post

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.

post
post
6 min read

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
Link to this section

Consider this simple example:

<>Copy
interface 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:

<>Copy
function 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

<>Copy
const 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:

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:

<>Copy
function 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:

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:

<>Copy
class 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?

<>Copy
class 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:

<>Copy
function 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 types
Link 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:

<>Copy
function 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:

<>Copy
const 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:

<>Copy
function 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

Conclusion
Link 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.

Share

About the author

author_image

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

author_image

About the author

Armen Vardanyan

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

About the author

author_image

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

Looking for a JS job?
NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

Angularpost
13 September 20218 min read
Tracking user interaction area

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Angularpost
13 September 20218 min read
Tracking user interaction area

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Read more
AngularpostTracking user interaction area

13 September 2021

8 min read

Explore one of the most complex pieces of Taiga UI — ActiveZone directive that keeps an eye on what region user is working with. It touches on low-level native DOM events API, advanced RxJS and Dependency Injection, ShadowDOM and more!

Read more
Angularpost
7 September 202122 min read
Designing Angular architecture - Container-Presentation pattern

Designing architecture could be tricky, especially in the agile world, where requirement changes are frequent. So your design has to support that and provides extendibility without the need for serious modification. In such cases, you will find the Container-Presentation pattern instrumental.

micro frontendspost
6 September 202125 min read
Taking micro-frontends to the next level

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

micro frontendspost
6 September 202125 min read
Taking micro-frontends to the next level

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

Read more
micro frontendspostTaking micro-frontends to the next level

6 September 2021

25 min read

The micro-frontends concept has been out there for quite a while. We’ve been using this architecture in Wix since around 2013, long before it was even given this name. In this article I’d like to share some of the things we did in order to evolve the concept of developing big scale micro-frontends.

Read more