DEV Community

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

Posted on

2

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 💚.

SurveyJS custom survey software

Build Your Own Forms without Manual Coding

SurveyJS UI libraries let you build a JSON-based form management system that integrates with any backend, giving you full control over your data with no user limits. Includes support for custom question types, skip logic, an integrated CSS editor, PDF export, real-time analytics, and more.

Learn more

Top comments (1)

Collapse
 
raulmarcosl profile image
Raúl M

Terrific post, thank you!

Postgres on Neon - Get the Free Plan

No credit card required. The database you love, on a serverless platform designed to help you build faster.

Get Postgres on Neon

👋 Kindness is contagious

Please leave a ❤️ or a friendly comment on this post if you found it helpful!

Okay