The best way to represent a type where some properties are dependent on others is to have several shapes for that type.
For example, let's say we have a Product
with a status
that can be AVAILABLE, BOUGHT, and REJECTED, we also have some other properties that depend on the current status
of the product.
The type of this Product
would be something like this:
// 👎 Not so good
type Product = {
name: string;
status: 'AVAILABLE' | 'BOUGHT' | 'REJECTED';
paidPrice?: number; // Only exist when status is BOUGHT
rejectedReason?: string; // Only exist when status is REJECTED
}
What is wrong with this implementation? The type is too loose, Product
allows to have incorrect scenarios like a rejected product without rejectedReason
or a product that was bought without paidPrice
.
A better implementation would be:
// 👍 Better
type Product = { name: string } & (
| {
status: 'AVAILABLE';
}
| {
status: 'BOUGHT';
paidPrice: number;
}
| {
status: 'REJECTED';
rejectedReason: string;
}
);
With this new implementation TS can infer correctly the properties that rely on the status
, for example:
const product1 = {} as Product;
if (product1.status === 'BOUGHT') {
// ✅ paidPrice is inferred correctly here
const priceAccepted = product1.paidPrice;
}
if (product1.status === 'AVAILABLE') {
// 🚫 TS error, 'paidPrice' does not exist on the type
const priceAccepted = product1.paidPrice;
}