Using Conditional Properties in TypeScript: A Practical Example
In TypeScript, conditional properties allow us to create flexible and type-safe interfaces that adapt based on certain conditions. This is particularly useful when dealing with complex data structures where certain properties should only be present under specific circumstances. In this blog post, we’ll explore how to use conditional properties with a practical example involving reward groups.
The Scenario
Imagine we have a system that manages different types of rewards. Each reward can be of a specific type, such as “FINANCE” or “SHIPPING”.
Depending on the reward type, certain attributes should be included or excluded. For instance, financial rewards should include financial attributes, while shipping rewards should include shipping attributes. Additionally, we want to ensure that certain attributes are only included based on the reward type and reward on conditions.
Defining the Types
First, let’s define the basic types and interfaces we’ll be working with:
type RewardType = "FINANCE" | "SHIPPING" | "OTHER"; // Example values for RewardType
interface ItemConditionAttribute {
// Define the properties of ItemConditionAttribute here
}
interface RewardAttributes {
// Define the properties of RewardAttributes here
}
interface ShippingAttributes {
// Define the properties of ShippingAttributes here
}
interface FinanceAttributes {
// Define the properties of FinanceAttributes here
}
interface RewardGroupBase {
groupId: number;
rewardType: RewardType;
rewardOn: string;
itemConditionAttributes: ItemConditionAttribute[];
}
Using Conditional Types
To ensure that financeAttributes is only included when rewardType is “FINANCE” and rewardAttributes is not included when rewardOn is “Finance”, we can use conditional types. Here’s how we define the RewardGroup type:
type RewardGroup = RewardGroupBase & (
{ rewardType: "FINANCE"; rewardOn: "Finance"; financeAttributes: FinanceAttributes; rewardAttributes?: never; shippingAttributes?: never } |
{ rewardType: "SHIPPING"; rewardOn: Exclude<string, "Finance">; shippingAttributes: ShippingAttributes; financeAttributes?: never; rewardAttributes: RewardAttributes } |
{ rewardType: Exclude<RewardType, "FINANCE" | "SHIPPING">; rewardOn: Exclude<string, "Finance">; financeAttributes?: never; shippingAttributes?: never; rewardAttributes: RewardAttributes }
);
Explanation
Base Interface:
RewardGroupBase contains the common properties that are always present regardless of the reward type.
Conditional Types:
We use a union of three types to handle the conditional properties.
When rewardType is “FINANCE” and rewardOn is “Finance”, financeAttributes is required,
and rewardAttributes and shippingAttributes are not allowed.When rewardType is “SHIPPING” and rewardOn is not “Finance”, shippingAttributes is required, and financeAttributes is not allowed, but rewardAttributes is included.
For any other rewardType and rewardOn not being “Finance”, rewardAttributes is included, but neither financeAttributes nor shippingAttributes are included.
Example Usage
Here’s how you can use the RewardGroup type in practice:
const financeReward: RewardGroup = {
groupId: 1,
rewardType: "FINANCE",
rewardOn: "Finance",
itemConditionAttributes: [ /* properties */ ],
financeAttributes: { /* properties */ }
};
const shippingReward: RewardGroup = {
groupId: 2,
rewardType: "SHIPPING",
rewardOn: "Delivery",
itemConditionAttributes: [ /* properties */ ],
shippingAttributes: { /* properties */ },
rewardAttributes: { /* properties */ }
};
// This will cause a TypeScript error because financeAttributes is not allowed for rewardType "SHIPPING"
const invalidReward: RewardGroup = {
groupId: 3,
rewardType: "SHIPPING",
rewardOn: "Delivery",
itemConditionAttributes: [ /* properties */ ],
financeAttributes: { /* properties */ } // Error: financeAttributes
};