Working with TypeScript often involves precise type checking and sometimes necessitates repetitive type assertions to tell the compiler what we "know" about a variable's type. While the language's robust type system enhances code quality, sometimes it can get verbose, especially when dealing with DOM elements. This blog post aims to introduce a feature in TypeScript called User-Defined Type Guards, a way to reduce redundancy and make your TypeScript code more readable and maintainable.
The Problem
Imagine we have a div
element, and we want to perform several operations on its first child if that child is also a div
with certain attributes and classes.
let divElement!: HTMLDivElement;
if (
divElement.firstChild &&
divElement.firstChild.nodeType === Node.ELEMENT_NODE &&
(divElement.firstChild as HTMLElement).tagName === 'DIV' &&
(divElement.firstChild as HTMLElement).hasAttribute('data-item-id') &&
(divElement.firstChild as HTMLElement).classList.contains('item')
) {
(divElement.firstChild as HTMLElement).click();
}
The code checks if divElement.firstChild
is an HTMLElement
and then performs other checks. However, this approach becomes verbose as we repeatedly use as HTMLElement
to assert the type.
Introducing User-Defined Type Guards
TypeScript allows us to create our own type guards using functions that perform a runtime check and employ a special return type to inform TypeScript about the type within a specific context. This feature is called User-Defined Type Guards.
Creating a Type Guard Function
Here's how you can create a type guard function to check if a node is an HTMLElement
.
function isHTMLElement(node: Node): node is HTMLElement {
return node.nodeType === Node.ELEMENT_NODE;
}
The node is HTMLElement
is a type predicate. It informs the TypeScript compiler that when this function returns true
, the variable passed as node
should be considered an HTMLElement
.
Using the Type Guard
With the type guard function in place, we can refactor our original code as follows:
let divElement!: HTMLDivElement;
if (
isHTMLElement(divElement.firstChild) &&
divElement.firstChild.tagName === 'DIV' &&
divElement.firstChild.hasAttribute('data-item-id') &&
divElement.firstChild.classList.contains('item')
) {
divElement.firstChild.click();
}
Benefits
Reduced Redundancy: With a user-defined type guard, we reduce the need for repetitive type assertions.
Increased Readability: The code is easier to read and understand because it is less cluttered with type assertions.
Reusable Code: The
isHTMLElement
function can be reused in multiple places in the application, making your codebase DRY (Don't Repeat Yourself).
Conclusion
TypeScript offers powerful features to create robust, maintainable code. However, verbosity can sometimes become an issue. User-Defined Type Guards help alleviate this problem by providing a more elegant and clean way to perform type checking, leading to cleaner, more readable, and less error-prone code.