TypeScript allows us to not only create individual types, but combine them to create more powerful use cases and completeness.
There’s a concept called “Intersection Types” in TypeScript that essentially allows us to combine multiple types.
Intersection types allow us to reuse existing type definitions from anywhere, and create new combinations of them.
Let’s assume we have a checkout, which accepts both Stripe and PayPal as the payment provider, we could start with an Order
interface:
interface Order {
total: number;
currency: string;
}
We might then extend it to add the payment providers, and whether we used them or not:
// ❌ Not a great approach, but works
interface Order {
total: number;
currency: string;
paypal?: boolean;
stripe?: boolean;
}
This creates an optional property on each type. Personally, I’m not a fan of this approach, so perhaps we could improve it via a union type:
// ❌ Not a great approach, but works
interface Order {
total: number;
currency: string;
provider: 'stripe' | 'paypal';
}
Better? So far, yes! But now we need to take different information based on the payment provider.
Let’s assume the Stripe method uses a credit card property, and PayPal uses an email.
This then takes us back to square one, as we’d then have to make the credit card field and email address (that’s used for PayPal) optional:
// ❌ Not a great approach, but works
interface Order {
total: number;
currency: string;
provider: 'stripe' | 'paypal';
card?: number;
email?: string;
}
So, what can we do? Create more basic types. Enter, the intersection type.
This is not only easy to do, but is a far more readable and extendable pattern.
Plus, it leaves less room for error, given that none of the fields will be “optional”.
Let’s break things out into more types:
// ✅ Getting better...
interface Order {
total: number;
currency: string;
}
interface Stripe { card: number; }
interface PayPal { email: string; }
Looks far easier to digest what’s happening already, I like it. Let’s move forward and combine them then.
An intersection type is defined using the ampersand &
character, which makes it clear what’s happening.
An intersection returns a brand new, combined, type. For this, we’ll use the type
keyword too instead of an interface
:
interface Order {
total: number;
currency: string;
}
interface Stripe { card: number; }
interface PayPal { email: string; }
type StripeOrder = Order & Stripe; // { total: number; currency: string; card: number; }
type PayPalOrder = Order & PayPal; // { total: number; currency: string; email: string; }
Pay attention to the new type definitions, I’ve added what they are in the comments. Beautiful!
This is super clean and allows us to create simpler logic elsewhere in our application, instead of hacking detection optional properties and so forth.
This could then be combined with a Literal Type Guard to create an inferred typed solution:
interface Order {
total: number;
currency: string;
}
interface Stripe { card: number; }
interface PayPal { email: string; }
type StripeOrder = Order & Stripe;
type PayPalOrder = Order & PayPal;
const isStripe = (order: StripeOrder | PayPalOrder): order is StripeOrder => {
return 'card' in (order as StripeOrder);
};
const processOrder = (order: StripeOrder | PayPalOrder) => {
if (isStripe(order)) {
// ✅ order.card
// ❌ order.email
} else {
// ✅ order.email
// ❌ order.card
}
};
And that’s it, a nice little example of merging types in TypeScript to create more types!
🚀 There’s so much more to TypeScript that I can teach you. Fast track your skills overnight with my deep-dive TypeScript Courses and take your skills to the top.
Thanks for reading!