A Type Guard is a technique where Typescript gets type information of a variable after making a type check using a conditional such as an if
statement (or switch
statement), an else if
or an else
.
Table of Contents
Why Type Guards
Consider the following code. The formatAmount
accepts a union type number | string
as its argument. Inside the method, we use the parseInt
to convert the string
to number
The parseInt
accepts only string
. Since it may be possible that the user might send a number rather than a string, the TypeScript compiler rightly flags this code as an error.
1 2 3 4 5 6 7 8 9 10 | function formatAmount(money: number | string) { let formattedAmount = "Rs. " +parseInt(money) //ERROR console.log(formattedAmount) return formattedAmount } //Argument of type 'string | number' is not assignable to parameter of type 'string'. //Type 'number' is not assignable to type 'string'. |
We can use any like parseInt(money as any)
. But using any
defeats the very purpose of using the TypeScript.
The solution is to give the TypeScript compiler a hint about the type so that the parseInt(money) does not throw errors. The solution must not break the Type system like using the any
type. This is where the Type Guards comes in.
What is a Type Guard
The document describes the Type Guard as an expression that performs a runtime check that guarantees the type in some scope. This definition has two parts
- Performs a runtime check on the type
- Guarantees the type in the scope of the above check
There are five ways, by which we can Perform a runtime check.
- Using
typeof
keyword instanceof
keywordin
keyword- Custom type guard Using type predicates
- Discriminated Unions
Typeof Type Guard
A typeOf keyword returns the type of an identifier in TypeScript. We can use that to check the Type of money
variable. If it is a string, then we can proceed with the use of parseInt
. The code is as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 | function formatAmount(money: number | string) { let formattedAmount:string; if (typeof money == "string") { formattedAmount= "Rs. " +parseInt(money) } else { formattedAmount = "Rs. " +money } console.log(formattedAmount) return formattedAmount } |
Now, the error magically disappears. The if (typeof money == "string")
block acts as Type Guard, hinting to the compiler that the inside the if
block the money
is string
. Hence the money
is treated as a string
in the if
block. As number
in the else
block. Outside the if.. else
block it is treated as number | string
.
You can see verify it by hovering over the money
variable as shown in the image below.
But the typeof can only detect primitives types like number
, string
, boolean
, symbol
, undefined
, function
. For everything else, it returns object
.
InstanceOf Type Guard
InstanceOf checks if a value is an instance of a class or a constructor function. The syntax for using it is as follows.
1 2 3 | obj instanceof class |
It returns true
if the obj
is an instance of the class
, or it appears anywhere in the inheritance chain. Else it will return false
.
In the following code both Customer
& SalesPerson
extend the Person
class. They have a common method code()
.They also have additional methods buy()
(Customer class) and sell()
(SalesPerson class).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | class Person { name: string = ''; } class Customer extends Person { code() { console.log("Customer Code")} buy() { console.log("Bought")} } class SalesPerson extends Person { code() { console.log("SalesPerson Code")} sell() { console.log("Sold")} } |
The method getCode
get its argument as of type Person
as we want to use it for both Customer
& SalesPerson
. The obj.code()
method does not compile as the method code does not exist on the class Person
. But we know that both inherited classes Customer
& SalesPerson
implement it.
1 2 3 4 5 6 7 8 9 | function getCode(obj: Person) { obj.code(); //Property 'code' does not exist on type 'Person' } getCode(new Customer()) |
We cannot use typeof
here as it returns object
. But we can use the InstanceOf
type guard.
We check the type of obj
using obj instanceof Customer
. If it is true
then the TypeScript treats the obj
as Customer
. We use the same technique in the else
block, where it is obj
is treated as SalesPerson
.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function getCode(obj: Person) { if (obj instanceof Customer) { obj.code(); } else if (obj instanceof SalesPerson) { obj.code(); } } getCode(new Customer()) //Customer Code getCode(new SalesPerson()) //SalesPerson Code |
You can verify it by checking the intellisense. Inside the if (obj instanceOf customer)
block, the typescript correctly infers the type as Customer
, It shows the method Buy
But does not show the method Sell
While inside the if (obj instanceOf SalesPerson)
block it infers the type as SalesPerson
. Here it shows the method Sell
and does not show Buy
Outside both the blocks, the intellisense shows only one property name
, which is from the Person
class
In Operator
The in operator does not check the type, but it checks if a property exists on an object. The syntax is
1 2 3 | propertyName in objectName |
The right hand of the expression must be an object.
The car
in the following code has a property start
. We can use the in
operator to check for it using the 'start' in car
. It returns true.
1 2 3 4 5 6 7 8 9 10 11 12 | const car = { make: 'Honda', start() {} }; if ('start' in car) { console.log('Exists') //True } else { console.log('Not Exists') } |
The in
operator also works as a Type guard. In the getCode method below, we check if the buy
& sell
methods exist in the obj
. Inside the if
block, the compiler correctly infers the type as customer
& salesPerson
. Remember to use the unique property while using the in
. If you use the code
property, which exists in both, the compiler will throw the error as it exists in both the classes.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 | class Person { name: string = ''; } class Customer extends Person { code() { console.log("Customer Code")} buy() { console.log("Bought")} } class SalesPerson extends Person { code() { console.log("SalesPerson Code")} sell() { console.log("Sold")} } function getCode(obj: Customer|SalesPerson) { if ("buy" in obj) { obj.buy(); } else if ("sell" in obj) { obj.sell(); } } getCode(new Customer()) //Bought getCode(new SalesPerson()) //Sold |
Custom Type Guard / Type Predicates
Type Predicates allow us to specify our own custom logic or user defined Type guards. To define a custom type guard, we need to write a function whose return type is a type predicate.
A type predicate looks like propertyName is Type
. Where propertyName
(Must be the name of the function parameter) is of type Type
. If the function returns true
, it indicates that propertyName is Type
is true.
In the following IsCustomer
checks if the parameter obj
is of type customer
. Hence we define the return type as obj is Customer
. Our custom logic checks if the property buy
exists in the obj
. If yes then we return true
.
1 2 3 4 5 6 7 8 9 10 11 12 13 | function IsCustomer(obj: any): obj is Customer { //You can write your own logic here to determine if the obj is customer //return true if yes else false return (obj as Customer).buy != undefined //if ("buy" in obj) return true; //return false; } |
Once our custom type guard is ready, we can use it similar to other Type guards as shown below.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 | function getCode(obj: Customer|SalesPerson) { if (IsCustomer(obj)) { obj.buy(); } else { obj.sell(); } } getCode(new Customer()) //Customer Code getCode(new SalesPerson()) //SalesPerson Code |
Discriminated Unions
The Discriminated Unions is another way by which you can take advantage of the Type Guards. It is actually a pattern consisting of a common literal type property (Discriminant Property), Union types, Type aliases & Type guards.
You can read more on Discriminated Unions