newsletter

Mastering Discriminated Unions in TypeScript

3 min read
Copied

What Are Discriminated Unions

Often, applications require variables and functions that have multiple types of input or output. TypeScript has a special Union Type for such use cases, called Discriminated Unions.

Discriminated Unions in TypeScript are formed from two or more other types. A variable of discriminated union can be one of the types from the union.

Here's a simple example where a variable can hold either a number or a string:

typescript
let id: number | string; id = 100; // This code is correct id = "200"; // This code is correct // TypeScript Error: Type 'boolean' is not assignable to type 'string | number'. id = false;

Unions are also useful when combining string literals. A literal in TypeScript is a special type for constant string, for example:

typescript
let text: "TypeScript" = "TypeScript"; text = "TypeScript"; // This code compiles // TypeScript Error: Type '"JavaScript"' is not assignable to type '"TypeScript"'. text = "JavaScript";

By combining literals into unions you can express that a variable can only accept a certain set of values:

typescript
function print(value: string, align: "left" | "right" | "center") { // ... } print("TypeScript types", "left"); // TypeScript Error: "align" parameter should be one of: "left" | "right" | "center" print("TypeScript types", "middle");

When using discriminated unions, TypeScript will only allow to access properties or methods that are common to all types in the union. To use type-specific properties or methods, you need to use type guards for built-in types:

typescript
function getLength(obj: string | string[]) { if (typeof obj === "string") { return obj.length; // Return length of a string } return obj.length; // Return legnth of string array }
Copied

Discriminated Unions With Object Types

Discriminated Unions can combine object types too. Let's explore an example of geometrics shapes, that have common and different properties:

typescript
type Circle = { type: "circle"; radius: number; }; type Square = { type: "square"; size: number; };

Let's create a function that calculates a square for each shape:

typescript
function getSquare(shape: Circle | Square) { if (shape.type === "circle") { return Math.PI * shape.radius * shape.radius; } if (shape.type === "square") { return shape.size * shape.size; } } const circle: Circle = { type: "circle", radius: 10 }; const square: Square = { type: "square", size: 5 }; console.log("Circle square: ", getSquare(circle)); console.log("Circle square: ", getSquare(square));

Here a getSquare function accepts a shape parameter that can be one of 2 types: Circle or Square. When working with custom types you can use a property that will allow to distinguish types from each other. In our example it's a type property. Type guards here are straightforward:

typescript
function getSquare(shape: Circle | Square) { if (shape.type === "circle") { return Math.PI * shape.radius * shape.radius; } if (shape.type === "square") { return shape.square; } return 0; }

TypeScript is smart enough to know that we are working with Circle or Square type.

Copied

Using Discriminated Unions as Function Return Type

Another example where discriminated unions are useful - is a function return type. Let's explore an example of placing an order for a product by its ID and desired quantity. The return type of this function will be a discriminated union representing three possible states:

  • order successfully created
  • product not found
  • not enough product quantity in the warehouse

The naive approach is to declare a big object with properties that cover all three cases. But such implementation is cumbersome.

Discriminated unions offer a more elegant approach:

typescript
type OrderResponse = OrderCreated | ProductNotFound | NoProductQuantity; type OrderCreated = { type: "order-created"; orderId: string; message: string; }; type ProductNotFound = { type: "product-not-found"; message: string; }; type NoProductQuantity = { type: "no-product-quantity"; message: string; };

Now we can use this union type as a function's return type. We are getting products from a database or an external API, but for simplicity we will mock the data:

typescript
function orderProduct(productId: string, quantity: number): OrderResponse { const productsDatabase = { "Mobile Phone": { stock: 10 }, "Smart TV": { stock: 0 }, }; const product = productsDatabase[productId]; if (!product) { return { type: "product-not-found", message: "Product not found" }; } if (quantity > product.stock) { return { type: "no-product-quantity", message: "Insufficient product quantity" }; } return { type: "order-created", orderId: `order-${new Date().getTime()}`, message: "Order has been successfully created" }; }

In this function we are returning 3 types of responses: "order-created", "product-not-found", "no-product-quantity". To order a product let's call the method:

typescript
const orderResult = orderProduct("Mobile Phone", 1); switch (orderResult.type) { case "order-created": console.log(`Success: ${orderResult.message} (Order ID: ${orderResult.orderId})`); break; case "product-not-found": console.log(`Error: ${orderResult.message}`); break; case "no-product-quantity": console.log(`Error: ${orderResult.message}`); break; }

As you can see discriminated unions make code more readable and maintainable in such use cases.

Copied

Summary

Discriminated unions in TypeScript offer a robust way to manage different data types under a united type. They enhance type safety, improve code clarity, and ensure that all possible cases are handled, reducing runtime errors. Using discriminated unions as function input and output types will significantly improve the quality and maintainability of your TypeScript applications.

Hope you find this newsletter useful. See you next time.

Whenever you're ready, here's how I can help you:

The .NET Senior Playbook is built to:

  • Fast-track you from junior or mid-level to senior
  • Keep you growing as a senior
  • Help you beat any .NET interview

Covers everything: C#, ASP.NET Core, EF Core, system design — answer each question first, reveal the solution, and a test after every chapter proves it stuck. Finish, and you earn a verifiable certificate for your LinkedIn.

The .NET Senior Playbook
View the Playbook

Not sure where you stand? Take the free .NET Developer Level Test:

  • Find out your real level — Junior to Senior+
  • 15 minutes across 13 areas of C#, .NET, ASP.NET Core and System Design

No credit card required. When completed you get a personalized report: your level, your strongest areas, and where to focus next — the perfect way to benchmark yourself before diving into the Playbook.

Take the free test

Enjoyed this article? Share it with your network

Improve Your .NET and Architecture Skills

Join my community of 25,000+ developers and architects.

Each week you will get 1 practical tip with best practices and real-world examples.

Learn how to craft better software with source code available for my newsletter.

Join 25,000+ developers already reading
No spam. Unsubscribe any time.