Leveraging User-Defined Type Guards and Type Predicates in TypeScript

Photo by Collin on Unsplash

Leveraging User-Defined Type Guards and Type Predicates in TypeScript

TypeScript has gained immense popularity due to its ability to provide static type checking and enhance the development experience in JavaScript projects. As TypeScript evolves, it continues introducing powerful features allowing developers to write more reliable and maintainable code.

One such feature is user-defined type guards and type predicates. This article explores how these mechanisms empower developers to perform precise type checks and ensure safer code.

User-Defined Type Guards and Type Predicates

User-defined type guards in TypeScript enable developers to create custom functions that narrow down the type of a variable based on certain conditions. These functions serve as runtime checks that allow the TypeScript compiler to infer a specific type, enhancing type safety and enabling more accurate code analysis.

Type predicates are a key aspect of user-defined type guards. They are TypeScript expressions that return a type assertion in a specific format. When used in conjunction with user-defined type guards, they enable developers to express their intent to the compiler, allowing it to infer types accurately.

Example: Notification Service

Consider a scenario where you're building a notification system with different notification types, such as registration_successful and newsletter_subscribed. By leveraging user-defined type guards and type predicates, you can create precise checks for each notification type and handle them accordingly.

type NotificationType = "registration_successful" | "newsletter_subscribed";


/**
 * Base type for notifications
 */
interface NotificationBase {
    type: NotificationType;
    payload: unknown;
};

interface RegistrationSuccessfulNotification extends NotificationBase {
    type: "registration_successful",
    payload: {
        userId: string;
    }
}

interface NewsletterSubscribedNotification extends NotificationBase {
    type: "newsletter_subscribed";
    payload: {
        userId: string;
        interest: string;
    }
}

class NotificationService {
    /*
    * Type predicate to check if notification is for successful registration
    */
    private isRegistrationSuccessfulNotification(notification: NotificationBase): notification is RegistrationSuccessfulNotification  {
        return notification.type === "registration_successful";
    }

    /*
    * Type predicate to check if notification is for newsletter subscription
    */
    private isNewsletterSubscribedNotification(notification: NotificationBase): notification is NewsletterSubscribedNotification {
        return notification.type === "newsletter_subscribed";
    }

    async send(notification: NotificationBase) {
        if (this.isRegistrationSuccessfulNotification(notification)) {
            console.log(`Registration Successful Notification ${notification.payload.userId}`);
            return;
        }

        if (this.isNewsletterSubscribedNotification(notification)) {
            console.log(`Newsletter Subscribed Notification ${notification.payload.interest}`);
            return;
        }
    } 
}


async function main() {
    const notificationService = new NotificationService();
    const registrationSuccessfulNotification: RegistrationSuccessfulNotification = {
        type: "registration_successful",
        payload: {
            userId: "204a4bbd-ea53-4639-b48f-59a15bcf623b",
        }
    }

    const newsletterSubscribedNotification: NewsletterSubscribedNotification = {
        type: "newsletter_subscribed",
        payload: {
            userId: "204a4bbd-ea53-4639-b48f-59a15bcf623b",
            interest: "typescript"
        }
    }
    notificationService.send(registrationSuccessfulNotification);
    notificationService.send(newsletterSubscribedNotification);
}

main();

In this example, isRegistrationSuccessfulNotification and isNewsletterSubscribedNotification are type predicates.

These methods implicitly return booleans.

 private isNewsletterSubscribedNotification(notification: NotificationBase): notification is NewsletterSubscribedNotification {
       return notification.type === "newsletter_subscribed";
   }

Now, when we use this method if it returns true, TypeScript knows that notification is a NewsletterSubscribedNotification. We will be able to safely call NewsletterSubscribedNotification specific properties.

if (this.isNewsletterSubscribedNotification(notification)) {
    // notification is a NewsletterSubscribedNotification
    console.log(`Newsletter Subscribed Notification ${notification.payload.interest}`);
    return;
 }

As you delve into the world of user-defined type guards and type predicates, you'll discover their potential to make your TypeScript projects more resilient and error-resistant. With a deeper understanding of these mechanisms, you'll be well-equipped to tackle complex type-checking scenarios and elevate the quality of your codebase