Back to Blog
Lesson 18 of the TypeScript: Typing JavaScript with Confidence course
TypeScriptAugust 5, 20264 min read

Mastering Discriminated Unions for Type-Safe API Responses

Learn how to use Discriminated Unions in TypeScript to handle complex data states. Master exhaustive checks and improve your API response handling today.

TypeScriptDiscriminated UnionsNarrowingTypesArchitecture
A vintage typewriter outdoors displaying "AI ethics" on paper, symbolizing tradition meets technology.

Previously in this course, we explored The In Operator for Object Narrowing in TypeScript to distinguish between object shapes. While the in operator works for simple property checks, it becomes cumbersome when your data has complex, interdependent states.

In this lesson, we are introducing Discriminated Unions, the industry-standard way to model state in TypeScript. By using a shared "discriminator" property, we can create precise, type-safe structures that eliminate entire classes of bugs in our Task API client.

What are Discriminated Unions?

A Discriminated Union is a pattern where you provide a common literal property (the "discriminant") across several types. When you check the value of this property, TypeScript automatically knows which type you are dealing with.

Think of it like a remote control. Instead of checking if a remote has a "volume" button, a "channel" button, or a "power" button, you look at a specific label on the remote that tells you the model name. Once you know the model, you know exactly which buttons exist.

Defining the Discriminant

To create a discriminated union, every member of your union must share a property with a unique, constant value.

TYPESCRIPT
type TaskLoading = {
  status: "loading";
};

type TaskSuccess = {
  status: "success";
  data: { id: number; title: string };
};

type TaskError = {
  status: "error";
  message: string;
};

// This is our Discriminated Union
type TaskResponse = TaskLoading | TaskSuccess | TaskError;

In this example, the status property is our discriminator. Because its values ("loading", "success", "error") are literal types, TypeScript can distinguish between the three states perfectly.

Improving API Response Handling

Close-up of PHP code on a monitor, highlighting development and programming concepts.

When building our Task API client, we often receive varying responses. Instead of checking for the existence of properties (like data or message), we check the status.

TYPESCRIPT
function handleResponse(response: TaskResponse) {
  switch (response.status) {
    case "loading":
      console.log("Still fetching...");
      break;
    case "success":
      // TypeScript knows CE9178">'data' exists here
      console.log("Task loaded:", response.data.title);
      break;
    case "error":
      // TypeScript knows CE9178">'message' exists here
      console.error("Failed:", response.message);
      break;
  }
}

This approach is far more robust than checking if (response.data). It makes your architecture significantly more predictable, as discussed in Discriminated unions in TypeScript: Modeling state without bugs.

Implementing Exhaustive Checks

One of the most powerful features of Discriminated Unions is the ability to ensure you've handled every possible state. By using the never type, you can force the compiler to alert you if a new state is added to your union but forgotten in your logic.

TYPESCRIPT
function processTask(response: TaskResponse) {
  switch (response.status) {
    case "loading":
      return "Wait";
    case "success":
      return "Done";
    case "error":
      return "Fail";
    default:
      // If we add a new status, this line will error at compile-time
      const _exhaustiveCheck: never = response;
      return _exhaustiveCheck;
  }
}

This pattern, often referred to as the TypeScript Result Pattern, turns runtime logic errors into compile-time safety checks.

Hands-on Exercise: Refining the API Response

In our running project, let's update our TaskAPI to use this pattern.

  1. Create a TaskResult union with idle, fetching, and completed states.
  2. Write a function that accepts this union and logs a message specific to each state.
  3. Add a fourth state, retrying, and see how the compiler highlights the missing case in your switch statement.

Common Pitfalls

Close-up of a triangular warning sign indicating a slippery surface, fixed to a wooden post.

  • Non-literal discriminants: The discriminant property must be a string or number literal. Using a broad type like string will fail to narrow the union.
  • Missing discriminants: If you forget to add the status property to even one member of your union, TypeScript will not be able to perform the narrowing.
  • Over-complicating states: While powerful, don't create "union soup." If your states are largely identical, a simple interface might be more maintainable.

FAQ

Q: Can I use multiple discriminants? A: Yes, but it's usually unnecessary. One is sufficient to narrow the type completely.

Q: Is this better than if/else? A: Yes, because switch statements paired with never checks provide the exhaustive safety that if/else chains lack.

Q: Does this add runtime overhead? A: No. TypeScript strips these types during compilation; only the plain JavaScript remains.

Recap

Team members presenting a project in a modern office setting with a focus on collaboration.

Discriminated Unions are the cornerstone of writing type-safe state machines in TypeScript. By centering your data structures around a shared discriminator, you enable the compiler to verify your application logic, ensuring that every possible API state is accounted for and handled correctly.

Up next: We will look at how to safely handle data of unknown origin by moving from any to unknown.

Similar Posts