Type Guards are one of the key features of type-safe code.

The TypeScript Handbook describes type guards as:

Some expression that performs a runtime check that guarantees the type in some scope

The most important part of this description is that type guards are in fact a form of runtime check, which means the variable is of expected type at the moment of code execution.

Instead of using the any keyword to force the TypeScript compiler to stop complaining about an unknown property, it is much more readable to use Type Guards. They do not disable type checks in application code, and thus make it less error prone. Your code runs, your tests pass. The compiler is happy and so are you.

TypeScript comes with built-in type guards like typeofinstanceofin and literal type guards. They’re very useful, but have limited scope in modern web development.

Using instanceof

instanceof can be used to check if a variable is an instance of a given class. The right side of the instanceof operator needs to be a constructor function.

class Pet { }

class Dog extends Pet {
    bark() {
        console.log('woof');
    }
}

class Cat extends Pet {
    purr() { 
        console.log('meow');
    }
}

function example(foo: any) {
    if (foo instanceof Dog) {
        foo.bark(); // OK, foo is type Dog in this block
//      foo.purr(); // Error! Property 'purr' does not exist on type 'Dog'.
} 
    
    if (foo instanceof Cat) {
        foo.purr(); // OK, foo is type Cat in this block
//      foo.bark(); // Error! Property 'bark' does not exist on type 'Cat'.
    }
}

example(new Dog());
example(new Cat());

Using typeof

typeof is used when you need to distinguish between types numberstringboolean, bigintobjectfunctionsymbol, and undefined .

When trying to use other string constants (like customized Type) , the typeof operator will not error, but won’t work as expected either, which leads to bugs that are hard to track. These kinds of expressions will not be recognized as type guards.

Unlike instanceoftypeof will work with a variable of any type. In the example below, foo could be typed as number | string without issue.

function example(foo: any) {
    if (typeof foo === 'number') {
        console.log(foo + 100); // foo is type number
    }
    if (typeof foo === 'string') {
        console.log('not a number: '+ foo); // foo is type string
    }
}
example(23);
example('foo');

This prints:

123
not a number: foo

The tricky part is that typeof only performs shallow type-checking. It can determine if a variable is a generic object, but cannot tell the shape of the object. This will not work:

if (typeof foo === 'Dog')  // foo is maybe an Object, but it can not determine if it is a      
                           // type 'Dog' . to do that, we have to use the instanceof Method

Using in

The in operator does a safe check for the existence of a property on an object of union type. For example:

function example(foo: Dog | Cat) {
  if ('purr' in foo) {
    foo.purr();  // foo is narrowed Cat
  }
  else {
    foo.bark(); // foo is narrowed to Dog
  }
}

Using Literal Type Guards

Additionally, you can use =====!== and != to distinguish between literal values, and thus form simple type guards, like in the following example:

type ConfirmationState = 'yes' | 'no' | 'N/A';

function confirmationHandler(state: ConfirmationState) {
  if (state == 'yes') {
    console.log('User selected yes');
  } else if (state == 'no') {
    console.log('User selected no');
  } else {
    console.log('User has not made a selection yet');
  }
}

User Defined Type Guards

In real-life projects, you might want, to declare your own type guards with custom logic to help the TypeScript compiler determine the type. You will need to declare a function that serves as type guard using any logic you’d like.

This function— User Defined Type Guard function — is a function that returns a type predicate in the form of event is MouseEvent in place of a return type:

function handle(event: any): event is MouseEvent {
    // body that returns boolean
}
If the function returns true, TypeScript will narrow the type to MouseEvent in any block guarded by a call to the function. In other words, the type will be more specific. event is MouseEvent ensures the compiler that the event passed into the Type Guard is in fact a MouseEvent. For example:
function isDog(test: any): test is Dog {
  return test instanceof Dog;
}

function example(foo: any) {
    if (isDog(foo)) {
        // foo is type as a Dog in this block
        foo.bark();
    } else {
        // foo is type any in this block
        console.log("don't know what this is! [" + foo + "]");
    }
}

example(new Dog());
example(new Cat());

This prints:

woof
don’t know what this is! [[object Object]]

PS: Type guard functions don’t have to use typeof or instanceof, they can use more complicated logic. For example,  this code checks function´s parameter to determine whether it’s customType 'GeneralType' :

type GeneralType = 'type1' | 'type2';

function someFunction(arg1:any) {
  if (isCustomType(arg1)) {
    // Do something
  }
  // Continue function
}

function isCustomType(arg: any): arg is GeneralType {
  return ['type1', 'type2'].some(element => element === arg);
}

Being one of the most underestimated features of TypeScript, Type guards are incredibly useful for narrowing types and satisfying the compiler. They help ensure type-safety and promote code that is easier to maintain.

By Shabazz

Software Engineer, MCSD, Web developer & Angular specialist

Leave a Reply

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