DEV Community

Sachit
Sachit

Posted on • Edited on

Typescript: User Defined Type guards

In our previous post any vs unknown, we talked about type narrowing. Basically type narrowing is:

The process of refining types to more specific types than declared is called narrowing.

Source

For javascript primitives ( string, number, bigint, boolean, undefined, symbol, null) that can contain values, we can use the special typeof operator to narrow the type. For example:

// ensuring age is a number if we don't know the value
// eg. it might be an any or unknown
if ( typeof age === 'number' ) { ... }
// or
if (typeof signature === 'string' { ... }
Enter fullscreen mode Exit fullscreen mode

This is all good when we have a primitive, but what if we have an object, how do we use the same technique of narrowing. But before that, why would we even need to do that ?

So here's an example:

interface Pet {
  speak: () => void;
}

interface Fish extends Pet {
  swim: () => void;
}

interface Bird extends Pet {
  fly: () => void;
}

interface Cow extends Pet {
  milk: () => void;
}

// Essentially, the subject is a union type of Cat or Dog
function flyPet(subject: Fish | Bird | Cow ) {
  subject.fly();
}
Enter fullscreen mode Exit fullscreen mode

So the subject variable passed to the function flyPet, is a union type of Bird or Fish or Cow. Other than the speech, each pet have their own trait. For example, fish can swim and bird can fly.

So if we invoke the function flyPet, then we will get a compile time error:

Image description

But this is good when we know the subject type. Now to why we need this, imagine if we didn't know the type of subject, eg. it was any or unknown as the data might be coming from an external api or its legacy code that we don't know what the type is genuinely, that would result in a run time error instead of catching it at compile time.

function flyPet(subject: any ) {
  // compiler happy, but will result in a runtime error
  subject.fly();
}
Enter fullscreen mode Exit fullscreen mode

We can prevent those runtime errors by using the technique of type narrowing by creating user defined type guards. e.g.

function isBird(obj: any): obj is Bird {
  if (!obj.hasOwnProperty('fly') || !(typeof obj.fly === 'function')) {
    return false;
  }
  return true;
}
Enter fullscreen mode Exit fullscreen mode

Function breakdown: We assert that for an object to be a bird

  • It needs to have a property fly
  • And the property fly is an executable function

Now we can rewrite our flyPet function to be run time safe like this:

function flyPet(subject: any ): void {
  if (isBird(subject)) {
    subject.fly();
  }
}
Enter fullscreen mode Exit fullscreen mode

So when we use our type guard, we have basically narrowed down the type to be a bird and anything inside the if scope, will be a bird and hence we can use our subject to fly.

In summary, User defined Type guards allows us to:

  • Narrow down the type for objects
  • Prevent runtime errors that are not caught compile time.
  • Practice safe programming techniques
  • Catch errors and not break the user application / experience.

In the next post, we will look at the concept of User defined type guards using a very popular library called Zod and how it can make it easier to write user defined type guards.

Till then, happy guarding.

Top comments (0)