DEV Community

Cover image for Advanced Typescript: Type Predicates, Narrowing & func Overrides.
Manuel Artero Anguita 🟨
Manuel Artero Anguita 🟨

Posted on

Advanced Typescript: Type Predicates, Narrowing & func Overrides.

Let's say we're building an application related to Ticketing 🎟️. We're to handle vouchers, discount codes, monthly pass. passes?...

For this exercise we're focusing on three types of Tickets:

Event Ticket Coupon Pass
Usage Entry to an event Discount on a product Multiple uses over a specified time period
Expiration Only valid for a specific date Expires on a specific date Valid for a set period of time
Example Ticket to a concert 10% off at Keyboards Monthly gym membership pass

Auto-generated image from Midjourney showing three different tickets; one could be a Coupon, another a ticket and the last one a pass

Let's start with 3x Typescript models:

Three type definitions in typescript, we have the exact code below

Typical from tech posts, our models seem to be perfectly prepared for extracting common properties 🌈.

Highlight the common properties from the tree types

* If you're wondering why am I using type instead of interface you might check my take on Interface vs. Type

Note: the ticketType property is telling which type is fulfilling out of the 3 options. We're defining its type as an Union Type. We'll use it in a min.

type TicketType = 'event-ticket' | 'coupon' | 'pass';

type CommonTicket = {
  ticketType: TicketType;
  code: string;
  description?: string;
}

type EventTicket = CommonTicket & {
  ticketType: 'event-ticket';    
  event: {
    eventId: string;
    eventName: string;
    eventDate: Date;
  };
};

type Coupon = CommonTicket & {
  ticketType: 'coupon';
  discountAmount: number;
  minPurchaseAmount?: number;
  expirationDate?: Date;
};

type Pass = CommonTicket & {
  ticketType: 'pass';
  startDate: Date;
  endDate: Date;
  remainingUses?: number;
  service: {
    serviceId: string;
    serviceName: string;
  };
};

type Ticket = EventTicket | Coupon | Pass;
Enter fullscreen mode Exit fullscreen mode

So far, so good.


At some point in our code we will end with a function with different logic branches depending on the type of the ticket. 101% sure of this.

VsCode (and I guess any advanced editor) is able to help us across if statements:

In the third branch - we've checked for event-ticket and pass, so input Ticket is a Coupon - VsCode infers the type Coupon correctly:

Screenshot of a function in which we're checking first for event-ticket, then pass, then the IDE infers the correct typing

This is thanks to Control Flow Analysis.

But What if we extract an aux. function?

Screenshot with a typescript error (explained below)

Now we are getting a nasty Property 'event' does not exist on type 'Ticket'.

-> We're loosing the narrowing for ticket when extracting the type check to a function.

Type Predicates to the rescue

The trick here is to use a Type predicate

-function isAnEvent(ticket: Ticket) {
+function isAnEvent(ticket: Ticket): ticket is EventTicket {
  return ticket.ticketType === 'event-ticket';
}
Enter fullscreen mode Exit fullscreen mode

🧑‍🚀 I could have just jumped into the type predicate solution, but this way I'm trying to share my mental process)

isAnEvent() isn't returning just a boolean, now it's defining the type guard we need.

Directly from the docs:

Any time [..]isAnEvent()[..] is called with some variable, TypeScript will narrow that variable to that specific type if the original type is compatible.


Overloading

Another tool I recommend in our toolboxes 🧰: Function Overloads.

If we end up in a situation as this one:

function getTicket(type: TicketType, code: string): Promise<Ticket> {
  // when type is 'event-ticket' -> EventTicket
  // when type is 'coupon' -> Coupon
  // when type is 'pass' -> Pass
}
Enter fullscreen mode Exit fullscreen mode

We would like to tell Typescript, "Ey, we can tell the exact Narrowing for Ticket depending on input".

I tried using Generics at first. Looking for some kind of weird "Type-Bounding".


function getTicket<T extends Ticket>(type: TicketType, code: string): Promise<T> { ... }
Enter fullscreen mode Exit fullscreen mode

While the solution is far more simple.

Just writing it down does the thing:



function getTicket(type: "event-ticket", code: string): Promise<EventTicket>;

function getTicket(type: "coupon", code: string): Promise<Coupon>;

function getTicket(type: "pass", code: string): Promise<Pass>;

function getTicket(type: TicketType, code: string): Promise<Ticket> {
  // ...
}

Enter fullscreen mode Exit fullscreen mode

kind of .cpp feeling here right?

We are overloading getTicket(); Typescript will infer the exact type as a charm:

Image description

--

Thanks for reading 💚.

Top comments (1)

Collapse
 
raulmarcosl profile image
Raúl M

Terrific post, thank you!