Pattern Matching: How Discriminated Unions Enhance Your TypeScript Development

Angelo Gentile III
7 min readMar 27, 2024

--

Explore the technical nuances of TypeScript’s Discriminated Union Types and learn how they streamline code organization and ensure type integrity.

TypeScript Logo Image

What is TypeScript?

TypeScript is a “syntactic superset”¹ of Javascript, which means that any valid JavaScript code is also valid TypeScript code. TypeScript, however, extends JavaScript by adding optional static typing, amongst various other features, to provide more strict guidance and enhanced tooling support during the development process.

In this piece, I will be focusing on one particular case of static typing offered within TypeScript; discriminated union types. If TypeScript is new to you or you would like more information on the full functionality and capabilities of Typescript as a whole, please see the extensive docs here.

Otherwise, let’s get started.

Static Typing

Static typing is a programming language feature where variable types are determined during compilation rather than at runtime. In statically typed languages, like TypeScript, each variable is assigned a type during declaration, and the compiler checks that the types are used correctly throughout the codebase. This process helps catch type-related errors early in the development process, improving code reliability and maintainability. In many code editors, such as VSCode, developers receive fantastic error recognition even before the compilation process!

function add(num1: number, num2: number) { // static typing determining type
return num1 + num2
};

add('2', 3); // this would display an error (our parameter cannot be a string)
add(3, 4); // would return the correct value 7

In this basic example above you should notice the use of a : followed by a particular type, in this case, the built-in TypeScript number type. This signifies that the variable before the colon will be of type number. Therefore, when we try to pass a number as a string, such as in the first example, an error occurs in our IDE and during our compilation.

This is the basis of TypeScript and why it is so helpful during the development process. It adds built-in error catching before any compilation or runtime environment so developers can write cleaner, error-free code. Since TypeScript works prior to runtime, when the files are compiled to working JavaScript code, the static typing and any other TypeScript features do not persist into the production files.

The above code would compile as follows:

function add(num1, num2) { // notice the types would not be present
return num1 + num2
};

Discriminated Union Types

These types allow you to define a custom type, typically an object type or interface, that can hold values of several different types, each identified by a unique tag or discriminant. This tagging mechanism distinguishes one variant from another and enables pattern matching to handle each variant differently. Pretty cool right?

The Two Main Pieces of it’s Anatomy:

  1. Variants: A discriminated union type consists of multiple variants, each representing a distinct shape or state of the data structure. Each variant can have its own set of properties or fields, which may differ from one variant to another. This is what makes each section of the union type unique from one another. This can comprise simple values or more complex functionality and logic if at all necessary.
  2. Discriminant: The discriminant is a property or field that distinguishes one variant from another within the union type. It serves as a tag or label that indicates the specific variant of the data structure. Think of it as the common thread that each section of the union type has but it is unique to that specific variant, generally, it is a specified string value.
type Album =
{ genre: 'rock'; title: string; artist: string; }
| { genre: 'pop'; title: string; artist: string; hits: number }
| { genre: 'jazz'; title: string; artist: string; instrument: string }
| { genre: 'hip-hop'; title: string; artist: string; featuredArtists: string[]};

In the code above, I’ve defined a discriminated union type called Album, which encompasses various genres of music albums. Each variant of the Album type includes a shared discriminant, genre, distinguishing one album genre from another within our discriminated union type. This discriminant serves as a key identifier, enabling precise type designation for our data based on its corresponding value.

Each genre can share common properties, such as title and artist, along with additional properties specific to each genre:

  • For rock albums, there are no additional properties.
  • For pop albums, there is a hits property representing the number of chart-topping hits.
  • For jazz albums, there is an instrument property indicating the primary instrument used.
  • For hip-hop albums, there is a featuredArtists property listing the featured artists on the album.

Each genre can contain any amount of shared or unique key/value pairs so long as there remains one particular discriminant to differentiate between each object type.

Additionally, we could also write the same discriminate union type, Album, in a more readable, programmer-friendly syntax. Rather than building via literal object unions, we should leverage custom object types to provide a more accessible code.

// each genre is divided into its own custom type
type Rock = {
genre: 'rock';
title: string;
artist: string;
};

type Pop = {
genre: 'pop';
title: string;
artist: string;
hits: number
};

type Jazz = {
genre: 'jazz';
title: string;
artist: string;
instrument: string
};

type HipHop = {
genre: 'hip-hop';
title: string;
artist: string;
featuredArtists: string[]
};

// Album is still a discriminated union type as before
type Album = Rock | Pop | Jazz | HipHop

In the refactored code above, we have separated each piece of the original Album type into their custom object types for each genre.

What is the advantage of this syntactical change?

Firstly, it enhances the accessibility and readability of the codebase for other developers. By structuring types in this way, it becomes much simpler for programmers to discern which attributes pertain to specific music genres. If we need to restructure any of our custom object types, we can be sure that the correct type is being altered, preventing future bugs in our code.

In addition, this approach greatly increases the flexibility of the project and its associated types. Individual types can now be exported for external usage beyond the confines of our original discriminated union type. This increased modularity allows for the use of distinct music genres throughout our codebase, coupled with the ease of alteration mentioned above. As always, our goal as programmers is to develop the most effective, well-rounded code for current and future use. With this syntactical change, we are on the correct path to satisfying that goal!

What about pattern matching?

Pattern matching involves specifying different code blocks or patterns for each possible variant of a discriminated union type. When the value of a discriminated union type is evaluated, the code block corresponding to the matching variant is executed.

In TypeScript, this pattern matching can be easily achieved by utilizing a switch statement leveraging different cases based on the discriminant value received.

function printAlbumInfo(album: Album): void {
switch (album.genre) {
case 'rock':
console.log(`Rock Album: ${album.title} by ${album.artist}`);
break;
case 'pop':
console.log(`Pop Album: ${album.title} by ${album.artist}, Hits: ${album.hits}`);
break;
case 'jazz':
console.log(`Jazz Album: ${album.title} by ${album.artist}, Instrument: ${album.instrument}`);
break;
case 'hip-hop':
console.log(`Hip-Hop Album: ${album.title} by ${album.artist}, Featured Artists: ${album.featuredArtists.join(', ')}`);
break;
default:
console.log('Unknown genre');
break;
};
};

In this example, the printAlbumInfo function takes an Album as the input and prints information about the album based on its genre. Pattern matching is used with a switch statement to handle each variant (rock, pop, jazz, hip-hop) separately, logging the appropriate information available per option within our discriminated union type. The default case, provides our fallback in case the input does not match our designated pattern or does not contain the discriminant at all.

It ensures type safety by requiring complete handling of all possible variants and simplifies future code maintenance by localizing logic related to each genre into separate code blocks.

The Downsides to Discriminated Union Types

While it’s true that discriminated union types offer numerous benefits, providing essential type safety and robustness to your code, it’s also important to acknowledge the downsides associated with their usage.

  1. Complexity: As the number of variants within the discriminated union expands, the need to handle each case increases. This would lead to longer switch or match statements that can become difficult to maintain or understand, particularly in larger codebases. In essence, more variants correlate to more necessary checks.
  2. Coupling: Discriminated union types couple the representation of data variants with the logic that operates on them. Any changes to the structure of the data variants may require corresponding modifications to the pattern matching logic throughout the codebase. For example, if a new variant is added to a discriminated union type, all switch statements or match expressions that handle that type must be updated to accommodate the new variant. This can be slightly handled with a default, as we did above, but this should only be used as a fallback case because it will apply that logic to every variant not listed in the switch statement.
  3. Performance: In some cases, pattern-matching with discriminated union types may have performance implications, especially if the switch statements or match expressions involve complex or computationally intensive logic. While modern compilers and runtime environments can optimize pattern matching to some extent, developers should be mindful of potential performance impacts.

Conclusion

Discriminated union types in TypeScript offer a powerful tool for modeling complex data structures with clarity and precision. Allowing developers to define custom types that can hold values of several different types, each identified by a unique tag or discriminant, discriminated union types ensure type integrity and can streamline your code organization and readability.

Throughout this exploration of discriminated union types, I’ve displayed how they can enhance your TypeScript development, leading to better, more effective code. From understanding the basics of static typing to exploring the very structure and anatomy of discriminated union types, I’ve demonstrated the robustness and type safety these types provide.

In addition to these benefits, it’s crucial to acknowledge the related downsides to discriminated union types. From potentially increased complexity to issues with coupling for future development, one must consider their unique code needs before implementing these types. While drawbacks do exist, they can be mitigated with proper design and consideration of trade-off value.

As a superset of JavaScript, TypeScript provides developers with increased type safety to develop better, more effective solutions. By leveraging the capabilities of TypeScript’s discriminated union types, developers can elevate their code and deliver better software solutions for current and future use!

Sign up to discover human stories that deepen your understanding of the world.

Free

Distraction-free reading. No ads.

Organize your knowledge with lists and highlights.

Tell your story. Find your audience.

Membership

Read member-only stories

Support writers you read most

Earn money for your writing

Listen to audio narrations

Read offline with the Medium app

--

--

Angelo Gentile III
Angelo Gentile III

Written by Angelo Gentile III

Full stack software engineer with a passion for garnering knowledge and continual growth. Always looking for something new!

No responses yet

Write a response