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; 
}