Categories
JavaScript TypeScript

TypeScript Advanced Types — Type Guards

Spread the love

TypeScript has many advanced type capabilities which make writing dynamically typed code easy. It also facilitates the adoption of existing JavaScript code since it lets us keep the dynamic capabilities of JavaScript while using the type-checking capabilities of TypeScript.

There are multiple kinds of advanced types in TypeScript, like intersection types, union types, type guards, nullable types, and type aliases, and more. In this article, we’ll look at type guards.


Type Guards

To check if an object is of a certain type, we can make our own type guards to check for members that we expect to be present and the data type of the values. To do this, we can use some TypeScript-specific operators and also JavaScript operators.

One way to check for types is to explicitly cast an object with a type with the as operator. This is needed for accessing a property that’s not specified in all the types that form a union type.

For example, if we have the following code:


interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeCode: string;
}
let person: Person | Employee = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
};
console.log(person.name);

Then the TypeScript compiler won’t let us access the name property of the person object since it’s only available in the Person type but not in the Employee type. Therefore, we’ll get the following error:

Property 'name' does not exist on type 'Person | Employee'.Property 'name' does not exist on type 'Employee'.(2339)

In this case, we have to use the type assertion operator available in TypeScript to cast the type to the Person object so that we can access the name property, which we know exists in the person object.

To do this, we use the as operator, as we do in the following code:

interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeCode: string;
}
let person: Person | Employee = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
};
console.log((person as Person).name);

With the as operator, we explicitly tell the TypeScript compiler that the person is of the Person class, so that we can access the name property which is in the Person interface.


Type Predicates

To check for the structure of the object, we can use a type predicate. A type predicate is a piece code where we check if the given property name has a value associated with it.

For example, we can write a new function isPerson to check if an object has the properties in the Person type:

interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeCode: string;
}
let person: Person | Employee = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
};
const isPerson = (person: Person | Employee): person is Person => {
  return (person as Person).name !== undefined;  
}
if (isPerson(person)) {
  console.log(person.name);  
}
else {
  console.log(person.employeeCode);  
}

In the code above, the isPerson returns a person is Person type, which is our type predicate.

If we use that function as we do in the code above, then the TypeScript compiler will automatically narrow down the type if a union type is composed of two types.

In the if (isPerson(person)){ ... } block, we can access any member of the Person interface.

However, this doesn’t work if there are more than two types that form the union type. For example, if we have the following code:

interface Animal {
  kind: string;
}
interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeCode: string;
}
let person: Person | Employee | Animal = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
};
const isPerson = (person: Person | Employee | Animal): person is Person => {
  return (person as Person).name !== undefined;  
}
if (isPerson(person)) {
  console.log(person.name);  
}
else {
  console.log(person.employeeCode);  
}

Then the TypeScript compiler will refuse to compile the code and we’ll get the following error messages:

Property 'employeeCode' does not exist on type 'Animal | Employee'.Property 'employeeCode' does not exist on type 'Animal'.(2339)

This is because it doesn’t know the type of what’s inside the else clause since it can be either Animal or Employee. To solve this, we can add another if block to check for the Employee type as we do in the following code:

interface Animal {
  kind: string;
}
interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeCode: string;
}
let person: Person | Employee | Animal = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
};
const isPerson = (person: Person | Employee | Animal): person is Person => {
  return (person as Person).name !== undefined;  
}
const isEmployee = (person: Person | Employee | Animal): person is Employee => {
  return (person as Employee).employeeCode !== undefined;  
}
if (isPerson(person)) {
  console.log(person.name);  
}
else if (isEmployee(person)) {
  console.log(person.employeeCode);  
}
else {
  console.log(person.kind);  
}

In Operator

Another way to check the structure to determine the data type is to use the in operator. It’s like the JavaScript in operator, where we can use it to check if a property exists in an object.

For example, to check if an object is a Person object, we can write the following code:

interface Animal {
  kind: string;
}
interface Person {
  name: string;
  age: number;
}
interface Employee {
  employeeCode: string;
}
let person: Person | Employee | Animal = {
  name: 'Jane',
  age: 20,
  employeeCode: '123'
};
const getIdentifier = (person: Person | Employee | Animal) => {
  if ('name' in person) {
    return person.name;
  }
  else if ('employeeCode' in person) {
    return person.employeeCode
  }
  return person.kind;
  
}

In the getIdentifier function, we used the in operator as we do in ordinary JavaScript code. If we check the name of a member that’s unique to a type, then the TypeScript compiler will infer the type of the person object in the if block as we have above.

Since name is a property that’s only in the Person interface, then the TypeScript compiler is smart enough to know that whatever inside is a Person object.

Likewise, since employeeCode is only a member of the Employee interface, then it knows that the person object inside is of type Employee.

If both types are eliminated, then the TypeScript compiler knows that it’s Animal since the other two types are eliminated by the if statements.


Typeof Type Guard

For determining the type of objects that have union types composed of primitive types, we can use the typeof operator.

For example, if we have a variable that has the union type number | string | boolean, then we can write the following code to determine whether it’s a number, a string, or a boolean. For example, if we write:

const isNumber = (x: any): x is number =>{
    return typeof x === "number";
}
const isString = (x: any): x is string => {
    return typeof x === "string";
}
const doSomething = (x: number | string | boolean) => {
  if (isNumber(x)) {
    console.log(x.toFixed(0));
  }
  else if (isString(x)) {
    console.log(x.length);
  }
  else {
    console.log(x);
  }
}
doSomething(1);

Then we can call number methods as we have inside the first if block since we used the isNumber function to help the TypeScript compiler determine if x is a number.

Likewise, this also goes for the string check with the isString function in the second if block.

If a variable is neither a number nor a string then it’s determined to be a boolean since we have a union of the number, string, and boolean types.

The typeof type guard can be written in the following ways:

  • typeof v === "typename"
  • typeof v !== "typename"

Where “typename” can be be "number", "string", "boolean", or "symbol".


Instanceof Type Guard

The instanceof type guard can be used to determine the type of instance type.

It’s useful for determining which child type an object belongs to, given the child type that the parent type derives from. For example, we can use the instanceof type guard like in the following code:

interface Animal {
  kind: string;
}
class Dog implements Animal{
  breed: string;
  kind: string;
  constructor(kind: string, breed: string) {    
    this.kind = kind;
    this.breed = breed;
  }
}
class Cat implements Animal{
  age: number;
  kind: string;
  constructor(kind: string, age: number) {    
    this.kind = kind;
    this.age = age;
  }
}
const getRandomAnimal = () =>{
  return Math.random() < 0.5 ?
    new Cat('cat', 2) :
    new Dog('dog', 'Laborador');
}
let animal = getRandomAnimal();
if (animal instanceof Cat) {
  console.log(animal.age);
}
if (animal instanceof Dog) {
  console.log(animal.breed);    
}

In the code above, we have a getRandomAnimal function that returns either a Cat or a Dog object, so the return type of it is Cat | Dog. Cat and Dog both implement the Animal interface.

The instanceof type guard determines the type of the object by its constructor, since the Cat and Dog constructors have different signatures, it can determine the type by comparing the constructor signatures.

If both classes have the same signature, the instanceof type guard will also help determine the right type. Inside the if (animal instanceof Cat) { ... } block, we can access the age member of the Cat instance.

Likewise, inside the if (animal instanceof Dog) {...} block, we can access the members that are exclusive to the Dog instance.


Conclusion

With various type guards and type predicates, the TypeScript compiler can narrow down the type with conditional statements.

Type predicate is denoted by the is keyword, like pet is Cat where pet is a variable and Cat is the type. We can also use the typeof type guard for checking primitive types, and the instanceof type guard for checking instance types.

Also, we have the in operator checking if a property exists in an object, which in turn determines the type of the object by the existence of the property.

By John Au-Yeung

Web developer specializing in React, Vue, and front end development.

Leave a Reply

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