Welcome to this epic TypeScript series on Type Guards. In this short series of 4 articles, we’re going to teach you all about Type Guards - the what, why and how’s so you can uncover some of the more advanced features we’d love to teach you.
For reference, the 4 articles in this TypeScript series:
- Understanding TypeScript: typeof Type Guard (you’re here!)
- Understanding TypeScript: instanceof Type Guard
- Understanding TypeScript: User defined Type Guards
- TypeScript’s Literal Type Guards and “in” Operator
Enjoying the series? Come and master the whole TypeScript language with us across 2 courses, beginner concepts and finally advanced. Full of real-world examples, it’ll answer so many questions you have and will have. Click here to check out more of what you’ll learn with us!
Table of contents
Here’s the source code of what we’re going to learn today:
So, before we begin - what is a “Type Guard” in TypeScript?
A Type Guard is a way that we can get hold of type information after making a check inside a conditional such as an if
statement, an else if
or an else
. The main idea is that when we use a Type Guard we essentially get information and we get the type information given to us inside that conditional block.
“typeof” Type Guard
We call it the typeof Type Guard because we are using the typeof
operator in JavaScript. TypeScript is aware that we can use the typeof
operator much like the instanceof
as well.
What we want to do before we get started is understand how we can use the typeof
operator inside perhaps a function and then we get given the type information if, for example, our conditional was true
in a particular scenario.
To demonstrate this what we’re going to do is explain this with a function that doesn’t really mean anything, we’re just simply demonstrating the example.
function foo(bar: string | number) {
if(typeof bar === 'string') {
//string
}
//number
}
So in the function below we can see that bar
can be of type string or number. Inside our if
statement we are saying that bar
is of type string
value which means that after our if
statement TypeScript cleverly knows that we are using a number
.
Let’s demonstrate this. Inside our if
statement we can say bar.
function foo(bar: string | number) {
if(typeof bar === 'string') {
bar.
}
//number
}
We will be able to access String.anchor
, big
, blink
and these are all string prototype methods and they exist only on the string objects. We also have access to slice
, replace
and things that you’ll commonly be using with JavaScript and TypeScript.
If we now go ahead and remove this and further down where TypeScript cleverly knows that we are using a number add bar
and hover over this TypeScript will tell us that bar: string | number
.
function foo(bar: string | number) {
if(typeof bar === 'string') {
//string
}
bar
}
So it might be that we want to return bar to uppercase. Which would mean that our bar
below resulted in just being a number because we are returning at this point a string method. TypeScript is detecting what we can do with this information.
function foo(bar: string | number) {
if(typeof bar === 'string') {
return bar.toUpperCase();
}
//number
bar
}
If we add the dot to bar.
we would now see the number methods all available to us on the prototype.
function foo(bar: string | number) {
if(typeof bar === 'string') {
return bar.toUpperCase();
}
//number
bar.
}
It might be that we want to take this if statement and so something similar below with a number. So we could say, if(typeof bar === 'number')
and then have a return statement.
function foo(bar: string | number) {
if(typeof bar === 'string') {
return bar.toUpperCase();
}
if(typeof bar === 'number') {
return bar.toFixed(2);
}
}
We could completely safety check all of these, is it a string?, is it definitely a number? or we could let TypeScript work this out for itself as we have just seen.
Free eBook
Directives, simple right? Wrong! On the outside they look simple, but even skilled Angular devs haven’t grasped every concept in this eBook.
- Observables and Async Pipe
- Identity Checking and Performance
- Web Components <ng-template> syntax
- <ng-container> and Observable Composition
- Advanced Rendering Patterns
- Setters and Getters for Styles and Class Bindings
Understanding Type Guards
Now we are going to build out a better example for this. This time with a class.
class Song {
constructor(public title: string, public duration: string | number) {}
}
We have two options and we’re going to create our Type Guard to detect whether the duration is going to be a string or a number. If it’s a number we are going to parse that and then get the actual value back otherwise we are going to return a string.
First of all we will setup our function which will take in an item of type Song
. We will use this function twice to understand what the Type Guard can do for us. Our const songDurationFromString
will have a string to the song and then will get that value given back.
class Song {
constructor(public title: string, public duration: string | number) {}
}
function getSongDuration(item: Song) {
}
const songDurationFromString = getSongDuration(
new Song('Wonderful Wonderful', '05:31')
);
It might be that in our application we are dealing with different number formats, we might get given the milliseconds or we might get given the string value representation passed out for us. It might be that we get this from the DOM, from the database or other areas in our application.
So this makes sense as a nice utility function to get us the song duration whether it’s a string and it’s already been passed out or it’s just the milliseconds.
If we log this out console.log(songDurationFromString);
it won’t do anything so we need to implement our Type Guard. First of all if our item.duration
is already a string we just simply want to return it.
class Song {
constructor(public title: string, public duration: string | number) {}
}
function getSongDuration(item: Song) {
if (typeof item.duration === 'string') {
return item.duration;
}
}
const songDurationFromString = getSongDuration(
new Song('Wonderful Wonderful', '05:31')
);
console.log(songDurationFromString);
Otherwise we are then going to convert those milliseconds to look like a string. Above we mentioned that we would use the function twice to get a better understanding of it. So let’s do that. We are going to say get the song duration from milliseconds songDurationFromMS
. We will then instead of passing a string as the second argument, pass a millisecond date-stamp.
class Song {
constructor(public title: string, public duration: string | number) {}
}
function getSongDuration(item: Song) {
if (typeof item.duration === 'string') {
return item.duration;
}
}
const songDurationFromString = getSongDuration(
new Song('Wonderful Wonderful', '05:31')
);
console.log(songDurationFromString);
const songDurationFromMS = getSongDuration(
new Song('Wonderful Wonderful', 330000)
);
The next step is what do we do once we failed the first Guard:
if (typeof item.duration === 'string') {
return item.duration;
}
So if we are not dealing with a string, then it must be that we are dealing with a number. To double check this we can add return item.duration
and hover over it and TypeScript will tell us that Song.duration: number
. So we can see that we are definitely dealing with a number.
function getSongDuration(item: Song) {
if (typeof item.duration === 'string') {
return item.duration;
}
return item.duration;
}
Instead of returning what we are going to do is create a few constants. Firstly, we will de-structure the duration from the item. We’re then going to obtain the minutes se we can do const minutes = Math.floor(duration / 60000);
. This will give us the minutes based on the time stamp that we passed in. Next we need to deal with the seconds const seconds = Math.floor(duration / 1000) % 60;
. Then we need to compose that return string. So we can say return ${minutes}:${seconds}
;
function getSongDuration(item: Song) {
if (typeof item.duration === 'string') {
return item.duration;
}
const { duration } = item;
const minutes = Math.floor(duration / 60000);
const seconds = (duration / 1000) % 60;
return `${minutes}:${seconds}`;
}
Either way we are returning a string from this function but the way that our Type Guard is dealing with this if the duration is a string to begin with, we just simply assume that we have already passed it out and we just return it. Otherwise we are going to make a few calculations and get those minutes and seconds.
Now let’s log all of this out.
class Song {
constructor(public title: string, public duration: string | number) {}
}
function getSongDuration(item: Song) {
if (typeof item.duration === 'string') {
return item.duration;
}
const { duration } = item;
const minutes = Math.floor(duration / 60000);
const seconds = (duration / 1000) % 60;
return `${minutes}:${seconds}`;
}
const songDurationFromString = getSongDuration(
new Song('Wonderful Wonderful', '05:31')
);
//05:31
console.log(songDurationFromString);
const songDurationFromMS = getSongDuration(
new Song('Wonderful Wonderful', 330000)
);
//5:30
console.log(songDurationFromMS);
Here’s the source code running, check the console!
This post is to demonstrate the power of using a Type Guard with TypeScript. By using the typeof
operator TypeScript knows that once that check has successfully been passed that we are dealing with a, in this case, string inside of our if statement.
That is what TypeScript is saying too and it is clever enough to actually detect that if we’re not dealing with a string, we are dealing with our other alternative. That type information is indeed coming from our public property of duration, so the type information is flowing down into the function and we could wrap the second part of our function into another if statement just to double check the item.duration
is a number but because TypeScript is helping us out here we can be sure that we are in fact dealing with a number type.