TypeScript: Typeguards and Type Narrowing.

Hi,
In this post we will be exploring a feature of TypeScript called type guards. To be more precise we will be exploring the typeguard using the in operator.

Note: The in operator is javascript feature not TypeScript.

Consider the following scenario: I am writing a todo application. In my todo application I have two types of Todos:

  1. Anonymous Todo
  2. Validated Todo

Anonymous todo have text and id fields. Lets write an interface for it

interface AnonymousTodo {
    text: string;
    id: number;
}

Validated Todo is similar to anonymous todo but with two extra fields authorName and validationDate. Lets write interface for it too.

interface ValidatedTodo {
    text: string;
    id: number;
    authorName: string;
    validationDate: Date;
}

so far so good, now lets write a method which will print the todo to console. So if anonymous todo is passed we should prepend anonymous logging text and id, but if ValidatedTodo is passed we should prepend 🔒 before logging the todo details.

function printValidation(todo: AnonymousTodo | ValidatedTodo) {

}

so our function printValidation accepts both AnonymousTodo and ValidatedTodo. But if you try to console.log(log.authorName); you will get the following error:

Property 'authorName' does not exist on type 'ValidatedTodo | AnonymousTodo'.
  Property 'authorName' does not exist on type 'AnonymousTodo'.(2339)

Lets try to log id instead of authorName, that works fine. Now lets try to log text, yes thats also works fine finally lets try to log validationDate. we get similar error as before.

So why is that? This is because TypeScript wants to make sure we only access the properties which are available on both ValidatedTodo and AnonymousTodo, in our case these common properties are id and text. But we want to access authorName and validationDate too. How can we do that?

This is where Typeguard comes in. We can use the typeguard to narrow the type. So as of now todo can be one of the two types. It can be either of type AnonymousTodo or ValidatedTodo. So we need to narrow it down for TypeScript, so TypeScript will know which type it is and will allow to access the properties available on it instead of allowing us to only access common properties. If it does not make sense do not worry. I have example coming up. Hopefully it will clear things up

There are multiple different type of guards available eg: instanceof,typeof etc. But in our case as we are using interface we will narrow the type using the in operator. The in operator is javascript language feature which can be used to check if a property is present in an object.

const myObj = {apple: 20};
if ("apple" in myObj) console.log(`We have ${myObj.apple} apples`);

So in the above snippet property apple is present in the myObj so we get true and a message will be logged to console. so lets integrate this in our example. Before we do that lets create objects of both type:

const todoWithValidation: ValidatedTodo = { text: "Ping", id: 1, validationDate: new Date(), authorName: "admin" };
const todoWithoutValidation: AnonymousTodo = { text: "Pong", id: 1 };

By looking at the both object we can see that ValidatedTodo will always have validationDate and authorName. So we can tell TypeScript to all at these two properties to distinguish between the ValidatedTodo and AnonymousTodo and we can do that by adding a simple if check which checks for these properties using the in operator. lets write the code for this.

function printValidation(todo: AnonymousTodo | ValidatedTodo) {
    if ("authorName" in todo && "validationDate" in todo) {
        console.log(`🔒 ${todo.authorName}, ${todo.validationDate}, ${todo.text}`);
    } else {
        console.log(`Anonymous ${todo.id}, ${todo.text}`);
    }
}

Inside the else block you can only access the properties of AnonymousTodo and inside the if block you can only access the properties of ValidatedTodo and outside of these scope you can only access the common properties.

Bonus:
Instead of the if ("authorName" in todo && "validationDate" in todo) we can also use a type predicate function:

function isValidatedTodo(todo: AnonymousTodo | ValidatedTodo): todo is ValidatedTodo {
  return ("authorName" in todo && "validationDate" in todo);
}

Notice the return type type of the function. You can find more details on type predicate in the official docs. Thats all for now. If you want to play with code you can access it here.

19