Understanding TypeScript Discriminated Unions

Josiah T Mahachi
3 min readJan 25, 2024

--

Understanding TypeScript Discriminated Unions

Coming from a C# background, moving to TypeScript was a bit of a culture shock, but I am loving it. In this blog post I talk about a TypeScript feature which I found useful in my current work, discriminated union, also known as a tagged union or algebraic data type. This powerful feature enables more precise type-checking (wink-wink C#) and can significantly enhance the safety and clarity of your code. We recently used discriminated unions for components we built for a React Native app.

Let’s get an understanding of what discriminated unions are first.

What are Discriminated Unions?

Discriminated unions are a TypeScript feature that allows you to combine multiple types into a single type. Each type in the union has a common property (the discriminant) with literal types that TypeScript can use to differentiate between the types.

Why Use Discriminated Unions?

  1. Improved Type Safety: They ensure that certain code paths are only accessible with the correct type.
  2. Better Code Organization: They can represent complex data structures more clearly.
  3. Easier Maintenance: They help in catching errors during compile time, reducing runtime errors.

Basic Discriminated Union

To get a better understanding of the concept, let’s start with a simple example. Suppose we have an application that deals with different shapes, and we need to calculate the area for each shape.

type Circle = {
kind: 'circle';
radius: number;
};

type Square = {
kind: 'square';
sideLength: number;
};

type Shape = Circle | Square;

function getArea(shape: Shape): number {
switch (shape.kind) {
case 'circle':
return Math.PI * shape.radius ** 2;
case 'square':
return shape.sideLength ** 2;
}
}

Here, the kind property is the discriminant. It tells TypeScript which type of shape it's dealing with, allowing the getArea function to correctly calculate the area based on the shape type.

Handling More Complex Structures

Let’s look at a more complex scenario where we handle user actions in an application.

type LoadAction = {
type: 'load';
payload: { url: string };
};

type SaveAction = {
type: 'save';
payload: { data: string };
};

type Action = LoadAction | SaveAction;

function handleAction(action: Action) {
switch (action.type) {
case 'load':
console.log(`Loading from ${action.payload.url}`);
break;
case 'save':
console.log(`Saving data: ${action.payload.data}`);
break;
}
}

Here, we see that the type property is the discriminant. It allows the handleAction function to understand what action to perform based on the action type.

Best Practices

  1. Use Literal Types for Discriminant: Always use string literal types or numeric literal types for the discriminant property.
  2. Cover All Cases in Switch: Make sure to handle all possible cases in a switch statement when using discriminated unions. TypeScript’s exhaustive check will help with this.
  3. Keep It Simple: Don’t overuse discriminated unions. They are powerful, but not every scenario requires them.

Conclusion

TypeScript’s discriminated unions are a robust feature for handling complex data structures and logic flows in a type-safe manner. By understanding and utilizing this feature, developers can write more predictable and maintainable code. Remember, the power of TypeScript lies in its type system, and discriminated unions are a key part of making the most of this system.

--

--

Josiah T Mahachi

Full-stack Developer, C#, ASP.Net, Laravel, React Native, PHP, Azure Dev Ops, MSSQL, MySQL