Type guards
When working with union types, the compiler will still apply its strong typing rules to ensure type safety. As an example of this, consider the following code:
function addWithUnion( arg1 : string | number, arg2 : string | number ) { return arg1 + arg2; }
Here, we are defining a function named addWithUnion, which accepts two parameters, and returns their sum. The arg1 and arg2 arguments are union types, and can therefore be either a string or a number. Compiling this code, however, will generate the following error:
error TS2365: Operator '+' cannot be applied to types 'string | number' and 'string | number'.
What the compiler is telling us here is that within the body of the function, where it attempts to add arg1 to arg2, it cannot tell what type arg1 is at the point of trying to use it. Is it a string, or is it a number?
This is where type guards come in. A type guard is an expression that performs a check on our type, and then guarantees that type within its scope. Consider the following code:
function addWithTypeGuard( arg1 : string | number, arg2 : string | number ) : string | number { if( typeof arg1 ==="string") { // arg1 is treated as string within this code console.log('first argument is a string'); return arg1 + arg2; } if (typeof arg1 === "number" && typeof arg2 === "number") { // arg1 and arg2 are treated as numbers within this code console.log('both arguments are numbers'); return arg1 + arg2; } console.log('default return'); return arg1.toString() + arg2.toString(); }
Here, we have a function named addWithTypeGuard that takes two arguments, and is using our union type syntax to indicate that arg1 and arg2 can either be a string or a number.
Within the body of the code, we have two if statements. The first if statement checks to see whether the type of arg1 is a string. If it is a string, then the type of arg1 is treated as a string within the body of the if statement. The second if statement checks to see whether both arg1 and arg2 are of type number. Within the body of this second if statement, both arg1 and arg2 are treated as numbers. These two if statements are our type guards.
Note that our final return statement is calling the toString function on arg1 and arg2. All basic JavaScript types have a toString function by default, so we are, in effect, treating both arguments as strings, and returning the result. Let's take a look at what happens when we call this function with different combinations of types:
console.log(`addWithTypeGuard(1,2)= ${addWithTypeGuard(1,2)}`) ;
Here, we are calling the function with two numbers, and receive the following output:
both arguments are numbers addWithTypeGuard(1,2)= 3
This shows that the code has satisfied our second if statement. If we call the function with two strings, as follows:
console.log(`addWithTypeGuard("1","2")= ${addWithTypeGuard("1","2")}`) ;
We can see here that the first if statement is being satisfied:
first argument is a string addWithTypeGuard("1","2")= 12
Lastly, when we call the function with a number and a string, as follows:
console.log(`addWithTypeGuard(1,"2") = ${addWithTypeGuard(1,"2"")}`) ;
In this case, both of our type guard statements return false, and so our default return code is being hit:
default return addWithTypeGuard(1,"2")= 12
Type guards, therefore, allow you to check the type of a variable within your code, and then guarantee that the variable is of the type you expect within your block of code.