Our content is free thanks to ag-Grid

ag-Grid is the industry leading JavaScript datagrid

ag-grid.com

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

Consider this simple example:

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:

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

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:

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:

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?

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:

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

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:

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:

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:

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

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.

Discuss with community

Share

About the author

author_image
Armen Vardanyan

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
Armen Vardanyan

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

NxAngularCli
NxAngularCli
NxAngularCli

Featured articles

RxJSpost
21 January 20214 min read
RxJS in Angular: Part III

In my previous two articles we have discussed how to change our components which solve problems in imperative ways to do that in functional, reactive, RxJS way, and we of course had a lot of fun doing that.

RxJSpost
21 January 20214 min read
RxJS in Angular: Part III

In my previous two articles we have discussed how to change our components which solve problems in imperative ways to do that in functional, reactive, RxJS way, and we of course had a lot of fun doing that.

Read more
RxJSpostRxJS in Angular: Part III

21 January 2021

4 min read

In my previous two articles we have discussed how to change our components which solve problems in imperative ways to do that in functional, reactive, RxJS way, and we of course had a lot of fun doing that.

Read more
Angularpost
20 January 20216 min read
Angular and SOLID principles

In software engineering, making things work the first time is always easy. But, what if you want to add new functionalities to an existing code? Making iterations on an existing basis can be difficult to do without introducing bugs. This is where SOLID principles come into play.

Angularpost
20 January 20216 min read
Angular and SOLID principles

In software engineering, making things work the first time is always easy. But, what if you want to add new functionalities to an existing code? Making iterations on an existing basis can be difficult to do without introducing bugs. This is where SOLID principles come into play.

Read more
AngularpostAngular and SOLID principles

20 January 2021

6 min read

In software engineering, making things work the first time is always easy. But, what if you want to add new functionalities to an existing code? Making iterations on an existing basis can be difficult to do without introducing bugs. This is where SOLID principles come into play.

Read more
Angularpost
14 January 20216 min read
Demystifying Taiga UI root component: portals pattern in Angular

Just before new year we announced our new Angular UI kit library Taiga UI. If you go through Getting started steps, you will see that you need to wrap your app with the tui-root component. Let's see what it does and explore what portals are and how and why we use them.