Coming from a C# background, I don’t tend to use Discriminated Unions a whole awful lot. By Discriminated Unions, I mean having a method that may take multiple different types, or may return multiple different types.

If you’ve come from a C# world, it’s basically like method overloads when passing parameters into a method. And the other way, it allows you to make a method that could return one of many different types, that don’t necessarily have to have an inheritance relationship between them.

Let’s see how this might work :

class Plane {
}

class Car {
}

getVehicle() : Plane | Car {
    //Either of these are valid. 
    return new Plane();
    //return new Car();
}

It’s somewhat strange at first to get used to this ability if you’ve never used a language that uses unions. Most developers would instead make a “base” class of “Vehicle” and make both Plane and Car inherit from Vehicle, and then you don’t need a union. Which.. In my view is probably valid.

But lets say you can’t do that, and you are dealing with code that either returns a Plane or Car, *or* code that accepts a Plane or Car. You’re going to need to know at some point, which one you have. Because if the objects were identical, you probably wouldn’t need this union at all. Type checking in Typescript on the surface seemed easy, but I went down a bit of a rabbit hole through documentation and guides that weren’t always clear. So I want to try and simplify it down all right here, right now.

Using The TypeOf Operator

Javascript actually has a typeof operator itself that can tell you which type a variable is. As an example, we can do things like :

let variable1 = 'abc';
let variable2 = 123;
console.log(typeof variable1);//Prints "string"
console.log(typeof variable2);//Prints "number"

But.. This isn’t as helpful as you might think. Other than the fact that it returns the “type” as a string, which is rather unhelpful in of itself, it doesn’t work with complex types. For example :

let myCar = new Car();
console.log(typeof myCar);//Prints "object"

For all custom classes (Which, in modern JavaScript you will have many), the return type is only ever object. That’s because the typeof operator can only tell you which primitive type your variable is, but nothing beyond that.

For this reason, while it may be helpful for telling strings from numbers, anything more, typeof is out!

Using The InstanceOf Operator

That brings us to the instanceof operator. It’s actually rather simple! We can just change our code to work like so :

let myCar = new Car();
console.log(myCar instanceof Car);//Prints true

Works well and we can now tell if our variable is an instance of a car. But there is a caveat, and it’s around inheritance. Consider the following code :

class Car {
}

class Honda extends Car {
}

let myCar = new Honda();
console.log(myCar instanceof Car);//Prints true

Notice that even though our variable holds a “Honda”, it still returns true as a car. For the most part, this is how all programming languages work so we shouldn’t read too much into this “limitation” as it’s really just polymorphism at play, but it’s still something to keep in mind.

Alas, we have an issue! A really smart developer has come along and said that interfaces are the new hip thing, and we should switch all classes to interfaces in our front end code. So we have this :

interface Car {
}

let myCar = {} as Car;
console.log(myCar instanceof Car);//Error 'Car' only refers to a type, but is being used as a value here.

This time around, we don’t even get to run our code and instead we get :

'Car' only refers to a type, but is being used as a value here.

What’s going on here? Well.. the instanceof operator works with classes only, not interfaces. Gah! OK but actually there is a way to check this further!

Using The As Cast Operator

Consider the following code (And yes I know it’s a fairly verbose example, but should hopefully make sense!)

interface Car {
  carMake : string;
}

let myCar = {carMake : 'Honda'};

let processCar = (car : object) => {
  //Some other code. 
  if(car as Car){
    console.log((car as Car).carMake);
  }
}

processCar(myCar);

Notice how we can cast our variable to a Car, and check if it has a value (by using a truthy statement, it will be undefined otherwise). Casting is actually a great way to determine whether variables are instances of an interface.

Using Typescript Type Guards

One thing I want to point out about the above code is that we have had to actually cast the car twice. Notice that inside the console log, we had to cast again for intellisense to pick up we were using a Car.

console.log((car as Car).carMake);

Typescript has a way to deal with this however. It’s called “Type Guards”, and it allows you to write code that will not only check an object is a given type, but that Typescript from that point on can treat the variable as the type.

For example, we can create a custom type guard like so :

function isCar(car : any): car is Car{
  return (car as Car) !== undefined;
}

The magic sauce here is the return type. It’s actually “car is Car”. That tells Typescript that should this return true, the input variable can be treated as a Car from this point onwards. This allows us to change our code to instead be :

let myCar = {carMake : 'Honda'};

let processCar = (car : object) => {
  //Some other code. 
  if(isCar(car)){
    console.log(car.carMake);
  }
}

processCar(myCar);

Notice how inside our console log, we didn’t have to cast again to have it act as a car.

This is a really important concept when talking about type guards. It’s not just a way to create a nice fancy method that can be used to check types, it actually allows Typescript to start treating what could be an unknown object type, as a very specific one within that code block.

Wade Developer
👋 Hey, I'm Wade
Wade is a full-stack developer that loves writing and explaining complex topics. He is an expert in Angular JS and was the owner of tutorialsforangular.com which was acquired by Upmostly in July 2022.

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.