Advanced Typescript: Type Predicates, Narrowing & func Overrides.
Tags: typescript, javascript, oop, webdev
Published: 2023-03-21 14:37:57

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1swu21yr7lj137dv0mck.png)

Let's start with 3x Typescript models:

![Three type definitions in typescript, we have the exact code below](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5jitq1pi0fwq3oazk73t.png)

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

![Highlight the common properties from the tree types](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/hr8nw8e9nbn6x0nly4gw.png)

> \* If you're wondering why am I using `type` instead of `interface` you might [check my take on Interface vs. Type](https://dev.to/manuartero/my-take-on-interface-vs-type-hc8)

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.

```ts
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;
```  

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](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/pma7gdhhh8c967uv1qwy.png)

This is thanks to [Control Flow Analysis](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#control-flow-analysis).

**_But What if we extract an aux. function?_**

![Screenshot with a typescript error (explained below)](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ta4dmqo3r8mec36lsdx4.png)

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**](https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates)

```diff
-function isAnEvent(ticket: Ticket) {
+function isAnEvent(ticket: Ticket): ticket is EventTicket {
  return ticket.ticketType === 'event-ticket';
}
```

> 🧑‍🚀 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](https://www.typescriptlang.org/docs/handbook/2/functions.html#function-overloads).

If we end up in a situation as this one:

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

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

❌

```typescript
function getTicket<T extends Ticket>(type: TicketType, code: string): Promise<T> { ... }
```

While the solution is far more simple.

Just writing it down does the thing:

✅

```typescript

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> {
  // ...
}

```

_**kind of .cpp feeling here right?**_

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

![Image description](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/q35y0gpw2xrga3wt31yh.png)

--

Thanks for reading 💚.