Back to blogs
Blog
Writing in public
Longer posts — published here first, then cross-posted to Dev.to with a canonical URL back to this site.
Blog
Longer posts — published here first, then cross-posted to Dev.to with a canonical URL back to this site.
Back to blogs
Back to blogs
Posttypescript, unions, node, programming
Discriminated unions (also known as tagged unions) are a powerful TypeScript pattern that enables...
Date published

A guide to creating Android home screen widgets using Expo modules, complete with state management,...

Modify your nextauth client and add a callbacks section that will get the access token from the...

You can select the scopes you want to request from GitHub/your oauthprovider in the login page, and...
Discriminated unions (also known as tagged unions) are a powerful TypeScript pattern that enables type-safe handling of values that could be of different types. They're especially useful when dealing with API responses, state management, or any scenario where you need to handle different outcomes.
A discriminated union is a data structure where:
Consider our first example using regular union types:
1type ErrorResponse = {2 message: string;3 statusCode: number;4 cause: string;5};67type SuccessResponse = {8 statusCode: number;9 data: Record<string, unknown>;10};1112// Union type to represent the possible responses13type Result = ErrorResponse | SuccessResponse;
When working with this type, we need to use property checking to determine which type we're dealing with:
1const fetchedData = await fetchData("https://example.com/api");23// Check for a property that only exists in one variant4if ("message" in fetchedData) {5 // TypeScript narrows fetchedData to ErrorResponse6 return { error: fetchedData.message };7}89// TypeScript narrows fetchedData to SuccessResponse10const response = fetchedData.data;
This approach has several issues:
A better approach is to use a discriminated union with an explicit tag:
1type ErrorResponse = {2 message: string;3 statusCode: number;4 cause: string;5 response_type: "error"; // The discriminant6};78type SuccessResponse = {9 message: string;10 statusCode: number;11 data: Record<string, unknown>;12 response_type: "success"; // The discriminant13};1415type Result = ErrorResponse | SuccessResponse;
With this pattern, type narrowing becomes explicit and reliable:
1const fetchedData = await fetchData2("https://example.com/api");23if (fetchedData.response_type === "error") {4 // TypeScript knows fetchedData is ErrorResponse5 return { error: fetchedData.message };6}78// TypeScript knows fetchedData is SuccessResponse9const response = fetchedData.data;
In our example application, we've implemented API response handling in two ways:
1app.get("/create", async (c) => {2 const fetechedData = await fetchData("https://jsonplaceholder.typicode.com/posts");34 // implicit ErrorResponse because of the error key5 if ("message" in fetechedData) {6 return c.json({ error: fetechedData.message }, 500);7 }89 // The type here is SuccessResponse & Record<"error", unknown>10 if ("error" in fetechedData) {11 return c.json({ error: fetechedData.error }, 500);12 }1314 // implicit SuccessResponse because of the data key15 if ("data" in fetechedData) {16 return c.json({ message: fetechedData.data }, 500);17 }1819 // We have to handle every possible case to avoid type errors20 const response = fetechedData.data;21 return c.json(response, 200);22});
1app.get("/create", async (c) => {2 const fetechedData = await fetchData2("https://jsonplaceholder.typicode.com/posts");34 // Using our discriminator we can easily narrow the type5 if (fetechedData.response_type === "error") {6 return c.json({ error: fetechedData.message }, 500);7 }89 // TypeScript knows fetechedData must be SuccessResponse2 here10 const response = fetechedData.data;11 return c.json(response, 200);12});
type, kind, or tag.Discriminated unions provide a robust pattern for handling multiple related types in TypeScript. By adding a specific property with a unique value for each type variant, you gain compile-time safety and improved code clarity. This pattern is particularly valuable when dealing with operations that can produce different outcomes, like API responses or state transitions.