Distrito Telefónica. Innovation & Talent Hub

Back
Development

Advanced typography in TypeScript

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: Type of tickets we are to consider Auto-generated image with Midjourney showing 3 different tickets; coupon, ticket and pass

Let’s start with 3x Typescript models:

Type definitions in Typescript.

Type definitions in Typescript.

Typical from tech posts, our models seem to be perfectly prepared for extracting common properties 🌈
Highlight the common properties from the tree types

Highlight the common properties from the tree types

If you’re wondering why are we using type instead of interface, we may cover this particular in another post. In summary: prefer type over interface if you are defining object properties. Remember, it’s called Typescript , not InterfaceScript

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

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 are checking first for event-ticket; the IDE infers the correct typing.

Screenshot of a function in which we are checking first for event-ticket; 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)

Screenshot with a typescript error (explained below)

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

We are 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';
}

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.

We could have just jumped into the type predicate solution, but this way I’m trying to share my mental process.

Overloading

Another tool I recommend in our toolboxes 🧰 is Function Overload. Continuing with the example we’re using, 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
}
We would like to tell Typescript,

“Ey, we can tell the exact Narrowing for Ticket depending on input”.

For some reason, I tried using Generics at first. Looking for some kind of weird “Type-Bounding”.

While the solution is much simpler.

Sometimes it's enough to write what you need: ...kind of a .cpp feeling here, right?

We're overloading getTicket(); Typescript will infer the exact type like a charm:
Correct infer of types

Correct infer of types

In this post, we've explored type predicates, narrowing, and function overloading concepts in TypeScript. These are powerful tools for developers. With a few tricks, we can enhance our typing skills and improve the reliability and flexibility of our TypeScript code.

Thanks for taking the time to read this post.