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 |
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 am I using
type
instead ofinterface
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;
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'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';
}
🧑🚀 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
}
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> { ... }
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> {
// ...
}
kind of .cpp feeling here right?
We are overloading getTicket()
; Typescript will infer the exact type as a charm:
--
Thanks for reading 💚.
Top comments (1)
Terrific post, thank you!