Using Type Guards
TypeScript introduces a concept called 'Type Guards', a powerful way to narrow down the type of an object within a conditional block. This is particularly useful when dealing with union types where the type could be one of several options.
What is a Type Guard?
A Type Guard is a some form of check that narrows down the type of a variable within a certain scope. This is typically done using some type checking functions or operators.
TypeScript defines a couple of different methods we can use to narrow down types. Let's dive into these methods now.
typeof Type Guards
The typeof
type guard is used to discriminate types based on their typeof
identifier. This is beneficial when dealing with primitive types like string
, number
, boolean
, object
, symbol
, and undefined
.
function doSomething(x: string | number) {
if (typeof x === 'string') {
// In this block TypeScript knows that `x` must be a string
console.log(x.subtr(1)); // OK
} else {
// In this block TypeScript knows that `x` must be a number
console.log(x.toFixed(2)); // OK
}
}
In the example above, TypeScript is aware of the type of x
in each block due to the typeof
type guard.
instanceof Type Guards
The instanceof
type guard is useful when dealing with class instances. It checks if an object is an instance of a specific class or constructor function.
class Foo {
foo = 0;
common = 'common';
}
class Bar {
bar = 0;
common = 'common';
}
function doSomething(arg: Foo | Bar) {
if (arg instanceof Foo) {
console.log(arg.foo); // OK
console.log(arg.bar); // Error
} else {
console.log(arg.foo); // Error
console.log(arg.bar); // OK
}
}
In this example, TypeScript knows that arg
is of type Foo
within the first block, and Bar
within the second block, thanks to the instanceof
type guard.
User-Defined Type Guards
We can also define our own type guards. These are functions which return a boolean, telling us whether an object is of a certain type or not.
interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
let pet: Fish | Bird;
// 'pet is Fish' is our type guard
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
In this example, isFish
is a User-Defined Type Guard. It checks whether the pet
can swim
or not and narrows down the type accordingly.
Conclusion
Type Guards in TypeScript allow us to narrow down types within our code, providing us with more type safety. They can be particularly useful when dealing with union types, but can also be used in a variety of different scenarios.
By combining the power of typeof
, instanceof
, and user-defined type guards, we can ensure that our code is more robust and less prone to runtime errors.