Learn how to use discriminated unions in TypeScript to create type-safe unions of interfaces, enhancing your code's reliability and maintainability.
In this section, we will explore the concept of discriminated unions in TypeScript. Discriminated unions are a powerful feature that allows you to create type-safe unions of interfaces, making your code more reliable and easier to maintain. By the end of this section, you’ll understand how to define discriminated unions, how TypeScript uses them for type narrowing, and the benefits they offer in pattern matching and exhaustive checking.
Discriminated unions, also known as tagged unions or algebraic data types, are a way to combine multiple types into a single union type. Each type in the union has a common property, known as the discriminant, which TypeScript uses to determine which type is currently in use. This allows TypeScript to narrow down the type and provide type safety when accessing properties specific to each type.
To create a discriminated union, you need to define a set of types that share a common discriminant property. This property is usually a string literal type, such as type
or kind
, which uniquely identifies each type in the union.
Here’s a basic structure of a discriminated union:
interface Circle {
kind: 'circle';
radius: number;
}
interface Square {
kind: 'square';
sideLength: number;
}
interface Rectangle {
kind: 'rectangle';
width: number;
height: number;
}
type Shape = Circle | Square | Rectangle;
In this example, we have defined three interfaces: Circle
, Square
, and Rectangle
. Each interface has a kind
property that acts as the discriminant. The Shape
type is a union of these interfaces.
TypeScript uses the discriminant property to narrow down the type of a variable. This means that when you check the value of the discriminant, TypeScript can infer the specific type and allow you to access properties unique to that type.
Let’s see this in action with a function that calculates the area of a shape:
function calculateArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius * shape.radius;
case 'square':
return shape.sideLength * shape.sideLength;
case 'rectangle':
return shape.width * shape.height;
default:
// This should never happen if all cases are covered
throw new Error('Unknown shape');
}
}
In the calculateArea
function, we use a switch
statement to check the kind
property of the shape
parameter. TypeScript automatically narrows the type based on the value of kind
, allowing us to safely access properties like radius
, sideLength
, and width
.
Discriminated unions offer several benefits that can improve the quality and maintainability of your code:
Type Safety: By using a common discriminant property, TypeScript can ensure that you only access properties that exist on the current type. This prevents runtime errors caused by accessing undefined properties.
Pattern Matching: Discriminated unions make it easy to implement pattern matching, a common technique in functional programming. This allows you to handle different types in a union with clear and concise code.
Exhaustive Checking: TypeScript can check that all possible cases are handled in a switch
statement, ensuring that your code is robust and free of logical errors.
Readability: By clearly defining the structure of each type and using a common discriminant, your code becomes more readable and easier to understand.
Now that we’ve covered the basics of discriminated unions, let’s put your knowledge into practice with some exercises. Try implementing the following scenarios using discriminated unions:
Define a discriminated union for different types of vehicles. Each vehicle type should have a kind
property and specific properties unique to that type. Implement a function that returns a description of the vehicle based on its type.
interface Car {
kind: 'car';
make: string;
model: string;
}
interface Truck {
kind: 'truck';
capacity: number;
}
interface Motorcycle {
kind: 'motorcycle';
engineCapacity: number;
}
type Vehicle = Car | Truck | Motorcycle;
function describeVehicle(vehicle: Vehicle): string {
switch (vehicle.kind) {
case 'car':
return `Car: ${vehicle.make} ${vehicle.model}`;
case 'truck':
return `Truck with capacity: ${vehicle.capacity} tons`;
case 'motorcycle':
return `Motorcycle with engine capacity: ${vehicle.engineCapacity} cc`;
default:
throw new Error('Unknown vehicle type');
}
}
Create a discriminated union for different payment methods. Each method should have a type
property and specific properties for that method. Write a function that processes a payment based on its type.
interface CreditCard {
type: 'creditCard';
cardNumber: string;
expiryDate: string;
}
interface PayPal {
type: 'paypal';
email: string;
}
interface BankTransfer {
type: 'bankTransfer';
accountNumber: string;
bankCode: string;
}
type PaymentMethod = CreditCard | PayPal | BankTransfer;
function processPayment(payment: PaymentMethod): void {
switch (payment.type) {
case 'creditCard':
console.log(`Processing credit card payment for card number: ${payment.cardNumber}`);
break;
case 'paypal':
console.log(`Processing PayPal payment for email: ${payment.email}`);
break;
case 'bankTransfer':
console.log(`Processing bank transfer to account: ${payment.accountNumber}`);
break;
default:
throw new Error('Unknown payment method');
}
}
To deepen your understanding of discriminated unions, try modifying the code examples above. For instance, add a new type of shape or vehicle and update the functions to handle the new type. Experiment with different discriminant properties and see how TypeScript’s type narrowing helps you write safer code.
To help you visualize how discriminated unions work, let’s use a flowchart to represent the calculateArea
function. This flowchart shows how the kind
property is used to determine the type of shape and calculate the area accordingly.
graph TD; A[Start] --> B{Check kind} B -->|circle| C[Calculate area: π * radius²] B -->|square| D[Calculate area: sideLength²] B -->|rectangle| E[Calculate area: width * height] C --> F[Return area] D --> F E --> F F --> G[End]
Discriminated unions are a powerful feature in TypeScript that allow you to create type-safe unions of interfaces. By using a common discriminant property, you can leverage TypeScript’s type narrowing to write safer and more maintainable code. Discriminated unions also make it easy to implement pattern matching and ensure exhaustive checking, improving the robustness of your code.
As you continue your journey with TypeScript, consider using discriminated unions in your projects to take advantage of these benefits. With practice, you’ll become more comfortable with this feature and be able to apply it effectively in a variety of scenarios.