Simplifying TypeScript Code with User-Defined Type Guards
Aug 31, 2023

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.

Categories

TypeScript