Type Guards are one of the key features of type-safe code.
The TypeScript Handbook describes type guards as:
Some expression that performs a runtime check that guarantees the type in some scope
The most important part of this description is that type guards are in fact a form of runtime check, which means the variable is of expected type at the moment of code execution.
Instead of using the any
keyword to force the TypeScript compiler to stop complaining about an unknown property, it is much more readable to use Type Guards. They do not disable type checks in application code, and thus make it less error prone. Your code runs, your tests pass. The compiler is happy and so are you.
TypeScript comes with built-in type guards like typeof
, instanceof
, in
and literal type guards. They’re very useful, but have limited scope in modern web development.
Using instanceof
instanceof
can be used to check if a variable is an instance of a given class. The right side of the instanceof
operator needs to be a constructor function.
class Pet { }
class Dog extends Pet {
bark() {
console.log('woof');
}
}
class Cat extends Pet {
purr() {
console.log('meow');
}
}
function example(foo: any) {
if (foo instanceof Dog) {
foo.bark(); // OK, foo is type Dog in this block
// foo.purr(); // Error! Property 'purr' does not exist on type 'Dog'.
}
if (foo instanceof Cat) {
foo.purr(); // OK, foo is type Cat in this block
// foo.bark(); // Error! Property 'bark' does not exist on type 'Cat'.
}
}
example(new Dog());
example(new Cat());
Using typeof
typeof
is used when you need to distinguish between types number
, string
, boolean
, bigint
, object
, function
, symbol
, and undefined
.
When trying to use other string constants (like customized Type) , the typeof
operator will not error, but won’t work as expected either, which leads to bugs that are hard to track. These kinds of expressions will not be recognized as type guards.
Unlike instanceof
, typeof
will work with a variable of any type. In the example below, foo
could be typed as number
| string
without issue.
function example(foo: any) {
if (typeof foo === 'number') {
console.log(foo + 100); // foo is type number
}
if (typeof foo === 'string') {
console.log('not a number: '+ foo); // foo is type string
}
}
example(23);
example('foo');
This prints:
123
not a number: foo
The tricky part is that typeof
only performs shallow type-checking. It can determine if a variable is a generic object, but cannot tell the shape of the object. This will not work:
if (typeof foo === 'Dog') // foo is maybe an Object, but it can not determine if it is a
// type 'Dog' . to do that, we have to use the instanceof Method
Using in
The in
operator does a safe check for the existence of a property on an object of union
type. For example:
function example(foo: Dog | Cat) {
if ('purr' in foo) {
foo.purr(); // foo is narrowed Cat
}
else {
foo.bark(); // foo is narrowed to Dog
}
}
Using Literal Type Guards
Additionally, you can use ===
, ==
, !==
and !=
to distinguish between literal values, and thus form simple type guards, like in the following example:
type ConfirmationState = 'yes' | 'no' | 'N/A';
function confirmationHandler(state: ConfirmationState) {
if (state == 'yes') {
console.log('User selected yes');
} else if (state == 'no') {
console.log('User selected no');
} else {
console.log('User has not made a selection yet');
}
}
User Defined Type Guards
In real-life projects, you might want, to declare your own type guards with custom logic to help the TypeScript compiler determine the type. You will need to declare a function that serves as type guard using any logic you’d like.
This function— User Defined Type Guard function — is a function that returns a type predicate in the form of event is MouseEvent
in place of a return type:
function handle(event: any): event is MouseEvent {
// body that returns boolean
}
If the function returnstrue
, TypeScript will narrow the type toMouseEvent
in any block guarded by a call to the function. In other words, the type will be more specific.event is MouseEvent
ensures the compiler that theevent
passed into the Type Guard is in fact aMouseEvent
. For example:
function isDog(test: any): test is Dog {
return test instanceof Dog;
}
function example(foo: any) {
if (isDog(foo)) {
// foo is type as a Dog in this block
foo.bark();
} else {
// foo is type any in this block
console.log("don't know what this is! [" + foo + "]");
}
}
example(new Dog());
example(new Cat());
This prints:
woof
don’t know what this is! [[object Object]]
PS: Type guard functions don’t have to use typeof
or instanceof
, they can use more complicated logic. For example, this code checks function´s parameter to determine whether it’s customType 'GeneralType'
:
type GeneralType = 'type1' | 'type2';
function someFunction(arg1:any) {
if (isCustomType(arg1)) {
// Do something
}
// Continue function
}
function isCustomType(arg: any): arg is GeneralType {
return ['type1', 'type2'].some(element => element === arg);
}
Being one of the most underestimated features of TypeScript, Type guards are incredibly useful for narrowing types and satisfying the compiler. They help ensure type-safety and promote code that is easier to maintain.