Let's imagine you are building a payment page. You may choose to represent the payment method type as follow:


interface PaymentMethod {
  type: "credit" | "debit" | "paypal";
  cardNumber?: number;
  pin?: number;
  email?: string;
}


As we can see, it's a small interface but you end up with some optional properties (cardNumber, pin, email) that are required for some of the payment methods.


Problem


We would like to say explicitly to TypeScript that the type 'paypal' needs to have information about 'email', but does not care about 'cardNumber'. Because for now, if we have something like:


const paymentWithPaypal: PaymentMethod = {
  type: "paypal"
};

// This is valid for TypeScript, but does not satisfy our requirements! 
// We need to have information in the 'email' property here.


So in essence, it would be really cool to be able to associate the type to its required properties.


This is what we are going to achieve with a discriminated union type.


Solution


Let's define our PaymentMethod type again with a union type that satisfies our requirements:


type PaymentMethod =
  | { type: "credit"; cardNumber: string; }
  | { type: "debit"; cardNumber: string; pin: string; }
  | { type: "paypal"; email: string; };


Now if we take our previous example:


const paymentWithPaypal: PaymentMethod = {
  type: 'paypal'
}; // ERROR

// Now we have a TypeScript error which tells us that 'email' is missing!
//---------------------//
// Type '{ type: "paypal"; }' is not assignable to type 'PaymentMethod'.
// Property 'email' is missing in type '{ type: "paypal"; }' but required in type '{ type: "paypal"; email: string; }'
//---------------------//


Other examples for common web-app use cases


Shapes:


type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "rectangle"; width: number; height: number }
  | { kind: "square"; size: number };


Data / error / loading state:


type State =
  | {
      status: "loading";
    }
  | {
      status: "success";
      data: {
        id: string;
      };
    }
  | {
      status: "error";
      error: Error;
    };


Summary


Here's a recap of the benefits of using discriminated union types in TypeScript:


  • Type safety: By using a discriminated union type, you can ensure that your code handles all possible cases in a type-safe way. TypeScript will catch any potential errors at compile-time, rather than at runtime.


  • Code organization: A discriminated union type allows you to group related types together, which can help keep your code organized and easier to read.


  • Flexibility: A discriminated union type can represent a wide range of related types, without requiring you to create separate interfaces or classes for each one.


  • Reduced code duplication: A discriminated union type can help reduce code duplication by allowing you to handle related types in a single function or method, rather than having to repeat the same code for each type.


  • Improved maintainability: By using a discriminated union type, you can make your code more maintainable by reducing the amount of code you need to write and ensuring that all cases are handled in a type-safe way.


Overall, discriminated union types are a powerful feature of TypeScript that can help you write cleaner, more maintainable code. By grouping related types together in a single type, you can improve the organization of your code, reduce duplication, and ensure type safety.