Check out my books on Amazon at https://www.amazon.com/John-Au-Yeung/e/B08FT5NT62
Subscribe to my email list now at http://jauyeung.net/subscribe/
TypeScript has many advanced type capabilities which make writing dynamically typed code easy. It also facilitates the adoption of existing JavaScript code since it lets us keep the dynamic capabilities of JavaScript while using the type-checking capabilities of TypeScript.
There are multiple kinds of advanced types in TypeScript, like intersection types, union types, type guards, nullable types, and type aliases, and more. In this article, we’ll look at type guards.
Type Guards
To check if an object is of a certain type, we can make our own type guards to check for members that we expect to be present and the data type of the values. To do this, we can use some TypeScript-specific operators and also JavaScript operators.
One way to check for types is to explicitly cast an object with a type with the as
operator. This is needed for accessing a property that’s not specified in all the types that form a union type.
For example, if we have the following code:
interface Person {
name: string;
age: number;
}
interface Employee {
employeeCode: string;
}
let person: Person | Employee = {
name: 'Jane',
age: 20,
employeeCode: '123'
};
console.log(person.name);
Then the TypeScript compiler won’t let us access the name
property of the person
object since it’s only available in the Person
type but not in the Employee
type. Therefore, we’ll get the following error:
Property 'name' does not exist on type 'Person | Employee'.Property 'name' does not exist on type 'Employee'.(2339)
In this case, we have to use the type assertion operator available in TypeScript to cast the type to the Person
object so that we can access the name
property, which we know exists in the person
object.
To do this, we use the as
operator, as we do in the following code:
interface Person {
name: string;
age: number;
}
interface Employee {
employeeCode: string;
}
let person: Person | Employee = {
name: 'Jane',
age: 20,
employeeCode: '123'
};
console.log((person as Person).name);
With the as
operator, we explicitly tell the TypeScript compiler that the person
is of the Person
class, so that we can access the name
property which is in the Person
interface.
Type Predicates
To check for the structure of the object, we can use a type predicate. A type predicate is a piece code where we check if the given property name has a value associated with it.
For example, we can write a new function isPerson
to check if an object has the properties in the Person
type:
interface Person {
name: string;
age: number;
}
interface Employee {
employeeCode: string;
}
let person: Person | Employee = {
name: 'Jane',
age: 20,
employeeCode: '123'
};
const isPerson = (person: Person | Employee): person is Person => {
return (person as Person).name !== undefined;
}
if (isPerson(person)) {
console.log(person.name);
}
else {
console.log(person.employeeCode);
}
In the code above, the isPerson
returns a person is Person
type, which is our type predicate.
If we use that function as we do in the code above, then the TypeScript compiler will automatically narrow down the type if a union type is composed of two types.
In the if (isPerson(person)){ ... }
block, we can access any member of the Person
interface.
However, this doesn’t work if there are more than two types that form the union type. For example, if we have the following code:
interface Animal {
kind: string;
}
interface Person {
name: string;
age: number;
}
interface Employee {
employeeCode: string;
}
let person: Person | Employee | Animal = {
name: 'Jane',
age: 20,
employeeCode: '123'
};
const isPerson = (person: Person | Employee | Animal): person is Person => {
return (person as Person).name !== undefined;
}
if (isPerson(person)) {
console.log(person.name);
}
else {
console.log(person.employeeCode);
}
Then the TypeScript compiler will refuse to compile the code and we’ll get the following error messages:
Property 'employeeCode' does not exist on type 'Animal | Employee'.Property 'employeeCode' does not exist on type 'Animal'.(2339)
This is because it doesn’t know the type of what’s inside the else
clause since it can be either Animal
or Employee
. To solve this, we can add another if
block to check for the Employee
type as we do in the following code:
interface Animal {
kind: string;
}
interface Person {
name: string;
age: number;
}
interface Employee {
employeeCode: string;
}
let person: Person | Employee | Animal = {
name: 'Jane',
age: 20,
employeeCode: '123'
};
const isPerson = (person: Person | Employee | Animal): person is Person => {
return (person as Person).name !== undefined;
}
const isEmployee = (person: Person | Employee | Animal): person is Employee => {
return (person as Employee).employeeCode !== undefined;
}
if (isPerson(person)) {
console.log(person.name);
}
else if (isEmployee(person)) {
console.log(person.employeeCode);
}
else {
console.log(person.kind);
}
In Operator
Another way to check the structure to determine the data type is to use the in
operator. It’s like the JavaScript in
operator, where we can use it to check if a property exists in an object.
For example, to check if an object is a Person
object, we can write the following code:
interface Animal {
kind: string;
}
interface Person {
name: string;
age: number;
}
interface Employee {
employeeCode: string;
}
let person: Person | Employee | Animal = {
name: 'Jane',
age: 20,
employeeCode: '123'
};
const getIdentifier = (person: Person | Employee | Animal) => {
if ('name' in person) {
return person.name;
}
else if ('employeeCode' in person) {
return person.employeeCode
}
return person.kind;
}
In the getIdentifier
function, we used the in
operator as we do in ordinary JavaScript code. If we check the name of a member that’s unique to a type, then the TypeScript compiler will infer the type of the person
object in the if
block as we have above.
Since name
is a property that’s only in the Person
interface, then the TypeScript compiler is smart enough to know that whatever inside is a Person
object.
Likewise, since employeeCode
is only a member of the Employee
interface, then it knows that the person
object inside is of type Employee
.
If both types are eliminated, then the TypeScript compiler knows that it’s Animal
since the other two types are eliminated by the if
statements.
Typeof Type Guard
For determining the type of objects that have union types composed of primitive types, we can use the typeof
operator.
For example, if we have a variable that has the union type number | string | boolean
, then we can write the following code to determine whether it’s a number, a string, or a boolean. For example, if we write:
const isNumber = (x: any): x is number =>{
return typeof x === "number";
}
const isString = (x: any): x is string => {
return typeof x === "string";
}
const doSomething = (x: number | string | boolean) => {
if (isNumber(x)) {
console.log(x.toFixed(0));
}
else if (isString(x)) {
console.log(x.length);
}
else {
console.log(x);
}
}
doSomething(1);
Then we can call number methods as we have inside the first if
block since we used the isNumber
function to help the TypeScript compiler determine if x
is a number.
Likewise, this also goes for the string check with the isString
function in the second if
block.
If a variable is neither a number nor a string then it’s determined to be a boolean since we have a union of the number, string, and boolean types.
The typeof
type guard can be written in the following ways:
-
typeof v === "typename"
-
typeof v !== "typename"
Where “typename”
can be be "number"
, "string"
, "boolean"
, or "symbol"
.
Instanceof Type Guard
The instanceof
type guard can be used to determine the type of instance type.
It’s useful for determining which child type an object belongs to, given the child type that the parent type derives from. For example, we can use the instanceof
type guard like in the following code:
interface Animal {
kind: string;
}
class Dog implements Animal{
breed: string;
kind: string;
constructor(kind: string, breed: string) {
this.kind = kind;
this.breed = breed;
}
}
class Cat implements Animal{
age: number;
kind: string;
constructor(kind: string, age: number) {
this.kind = kind;
this.age = age;
}
}
const getRandomAnimal = () =>{
return Math.random() < 0.5 ?
new Cat('cat', 2) :
new Dog('dog', 'Laborador');
}
let animal = getRandomAnimal();
if (animal instanceof Cat) {
console.log(animal.age);
}
if (animal instanceof Dog) {
console.log(animal.breed);
}
In the code above, we have a getRandomAnimal
function that returns either a Cat
or a Dog
object, so the return type of it is Cat | Dog
. Cat
and Dog
both implement the Animal
interface.
The instanceof
type guard determines the type of the object by its constructor, since the Cat
and Dog
constructors have different signatures, it can determine the type by comparing the constructor signatures.
If both classes have the same signature, the instanceof
type guard will also help determine the right type. Inside the if (animal instanceof Cat) { ... }
block, we can access the age
member of the Cat
instance.
Likewise, inside the if (animal instanceof Dog) {...}
block, we can access the members that are exclusive to the Dog
instance.
Conclusion
With various type guards and type predicates, the TypeScript compiler can narrow down the type with conditional statements.
Type predicate is denoted by the is
keyword, like pet is Cat
where pet
is a variable and Cat
is the type. We can also use the typeof
type guard for checking primitive types, and the instanceof
type guard for checking instance types.
Also, we have the in
operator checking if a property exists in an object, which in turn determines the type of the object by the existence of the property.