19
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:
- Anonymous Todo
- 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