Generally speaking, a guard in a method or function is a piece of code which checks some condition, and if that condition is false, exists the method/function it's in or otherwise skips a certain portion of code. When used correctly, guards can make the code base more resilient, less bug-prone, and more maintainable.
In TypeScript, there is also a concept called a type guard; this is a piece of code which determines if a certain variable or property adheres to some type definition, and if it does not, exists the method/function or skips a certain piece of code. In the sections below, we'll discuss how type guards work and how to effectively write and use them.
When To Use Type Guards
Type guards are best used in situations where we have union types, such as string | number, Cat | Dog | Horse, or any similar declaration. At any point we need to perform some action only if a variable is a specific type, we can use a type guard to:
- Determine what type the variable is in a way TypeScript can understand.
- Narrow the subsequent scope so that we can treat it as that type without the need to cast or keep type checking.
- Make our code cleaner and more maintainable.
The second point above provides the major benefit: we can use type guards to narrow the type definition within certain scopes of code, so once we know a variable is of one type or another, we can treat it as definitely being that type. For example:
In the sections below, we'll outline how to write the type guards using type predicates.
Type Predicates
A type predicate is a special type declaration which indicates that some variable or property does, or does not, qualify as a specified type. We can declare a type predicate by creating a method or function which performs some logic at runtime and returns a boolean value. The return type of that function/method however, instead of being a boolean, will be of the form: variable is type. In place of variable, we'll use the name of the variable being tested, and in place of type, we'll use the name of the type we're testing the variable for. The code below outlines an example where we're testing if a variable is of type Dog.
In the code above, we've defined a Dog class to help demo the concept. We've then created a function isDog, where we check if any given object is a dog by checking if it has a method called "bark". Note that it is possible there could be other classes that aren't dogs that have a bark method, so if you choose to use a type guard like this, it's up to you to ensure you're testing a given object properly. Lastly, the return type is animal is Dog. Notice that "animal" is the same name as that of the input argument. On the "test" function, the logic branches depending upon whether or not the dog test passes. If it does pass, TypeScript will treat animal inside of that initial if statement as a Dog, and will treat animal inside of the else statement as a Cat, as opposed to the union type Dog | Cat. In other words, TypeScript has narrowed down the type based on our type guard.
Guards Without Type Predicates
If we took the example above, but instead of stating the return type as item is Dog, we state it as a boolean, the function isDog itself will still work as expected and will still return the same value it did before. However, the test function will now break and prevent TypeScript from building! Without the type predicate, TypeScript will not be able to narrow the type declaration within the if/else branches and thus the code will not build.
Therefore, in order to leverage the type narrowing, be sure to use type predicates.