Discriminated union types in TypeScript

Apr 01, 2023#typescript

In TypeScript, discriminated union types are a special kind of union types that can be narrowed down using a common property that each member of the union contains. This property is called a discriminant or a tag.

For example, if you have a union type of different shapes, each with a kind property that indicates the shape type, you can use a switch statement or an if-else chain to narrow down the union type to a specific shape type based on the kind property.

type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; sideLength: number };

function getArea(shape: Shape) {
  if (shape.kind === "circle") {
    // (parameter) shape: { kind: "circle"; radius: number; }
    return Math.PI * shape.radius ** 2;
  } else {
    // (parameter) shape: { kind: "square"; sideLength: number; }
    return shape.sideLength ** 2;
  }
}

In this example, TypeScript knows that shape.kind can only be either “circle” or “square”, and it can narrow down the type of shape accordingly in each branch of the if-else statement. This way, you can access the specific properties of each shape type without getting any errors.

You do not have to use the name kind for the discriminant property, but it is a common convention.

One common use case for discriminated unions is to model the loading state of some data that is fetched asynchronously from a remote source. Here’s another example:

type UserProfile = {
  name: string;
  bio: string;
  avatar: string;
};

type UserProfileState =
  | { status: "loading" }
  | { status: "success"; data: UserProfile }
  | { status: "error"; error: Error };

// Example usage:

function UserProfileScreen() {
  // Assume we have a hook that fetches the user profile and returns a UserProfileState
  const userProfileState = useFetchUserProfile();

  // We can use a switch statement to check the status property and render different UI accordingly
  switch (userProfileState.status) {
    case "loading":
      return <LoadingSpinner />;
    case "success":
      return <UserProfileDisplay data={userProfileState.data} />;
    case "error":
      return <ErrorMessage error={userProfileState.error} />;
  }
}

By using a discriminated union type, you make it impossible to create a user profile state with inconsistent or missing information, or with some other status that is not supported by the app.