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:

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

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:

This is thanks to Control Flow Analysis.
But What if we extract an aux. function?

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:

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.
Related Content
Communication
Contact our communication department or requests additional material.
