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.

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.
TYPESCRIPTtype 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

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.
TYPESCRIPTfunction 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.
TYPESCRIPTfunction 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.
- Create a
TaskResultunion withidle,fetching, andcompletedstates. - Write a function that accepts this union and logs a message specific to each state.
- Add a fourth state,
retrying, and see how the compiler highlights the missing case in yourswitchstatement.
Common Pitfalls

- Non-literal discriminants: The discriminant property must be a string or number literal. Using a broad type like
stringwill fail to narrow the union. - Missing discriminants: If you forget to add the
statusproperty 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

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.
Work with me

Next.js Full-Stack Web App Development
A fast, SEO-ready full-stack web app built with Next.js 16 — from idea to deployed product, by an engineer who ships to production.

Laravel REST API Development
Clean, secure, well-documented Laravel REST APIs — the backend engine for your app, mobile client, or SaaS. Built by an API specialist.


