Custom type guards are functions that make it easier for TypeScript to identify the proper type of a variable during runtime. They can be especially useful when dealing with complex types made up of multiple interfaces or types, such as discriminated unions or intersection types.
Let’s go through the process of creating custom type guards step by step with an example.
Suppose we have a complex type that can be either a ‘Circle‘ or a ‘Rectangle‘, represented by the following interfaces:
interface Circle {
kind: 'circle';
radius: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
To define a custom type guard, you need a function that takes a parameter of a more general type (e.g., ‘unknown‘ or ‘any‘) and returns a boolean, with a ‘type predicate‘ as the return type. Here’s an example of a type guard for the ‘Circle‘ type:
function isCircle(shape: unknown): shape is Circle {
return (shape as Circle).kind === 'circle';
}
In the example above, we’re taking an ‘unknown‘ parameter ‘shape‘ and use a type assertion to check whether its ‘kind‘ property is equal to ‘circle‘. If this condition is true, TypeScript can infer that the ‘shape‘ parameter is of type ‘Circle‘.
Following the same pattern, you can create a type guard for the ‘Rectangle‘ type:
function isRectangle(shape: unknown): shape is Rectangle {
return (shape as Rectangle).kind === 'rectangle';
}
Now, let’s use our custom type guards with a function that calculates the area of a given shape:
type Shape = Circle | Rectangle;
function area(shape: Shape): number {
if (isCircle(shape)) {
return Math.PI * shape.radius * shape.radius;
} else if (isRectangle(shape)) {
return shape.width * shape.height;
} else {
throw new Error('Invalid shape type');
}
}
In the ‘area‘ function, we make use of our custom type guards ‘isCircle‘ and ‘isRectangle‘ to check whether the input shape is a ‘Circle‘ or a ‘Rectangle‘. Once the type guard is confirmed by TypeScript, we have access to the specific properties of that type, like ‘radius‘, ‘width‘, and ‘height‘.
To summarize, custom type guards can be created by defining a function that takes a parameter of a general type and returns a boolean with a type predicate. These type guards greatly enhance TypeScript’s ability to infer types during runtime, particularly for complex types made up of multiple interfaces or other types.