One of the key features of TypeScript is the powerful ability to use union types. These types allow you to combine multiple types together in powerful ways, making it easier to work with complex data structures and functions. In this article, I’ll explain Union types, and when to use them.

A union type is a type that represents a value that can be one of several different types. Union types can be especially useful when working with data that can take on different forms or when dealing with APIs that return data in multiple formats.

For example:

function length(value: string | any[]) {
    //
}

You might expect a length function to be able to handle strings or arrays, since it makes sense to use the length of either.

To use the value though, you’ll have to narrow down the type with some type checking:

function length(value: string | any[]) {
    if (typeof value === 'string') {
        return value.length; //Obviously in this case length works on both
    } else {
        return value.length;
    }
}

Inside the first if statement, TypeScript knows the value is a string, whereas, inside the second, TypeScript knows the value is an array.

Unions can be anything from primitives, to objects:

type Cat = {
    type: 'cat';
    meow(): void;
};

type Dog = {
    type: 'dog';
    woof(): void;
};

type Animal = Cat | Dog;

Discriminating Unions

Typescript is always trying to narrow down your types as logically as possible, and will cut out options from your union if it can. Let’s take this function for example, using our previous types:

function sound(pet: Cat | Dog) {

}

We know we have a cat if the type is cat, and a dog if the type is dog. And we also know that only a cat can meow, and only a dog can woof(). So how do we tell TypeScript this?

Simple, we just need to check for the type:

function sound(pet: Cat | Dog) {
    if (pet.type === 'cat') {
        pet.meow(); //pet: Cat
    } else {
        //Only two options, so it has to be a dog
        pet.woof();
    }
}

TypeScript knows that if the type string is ‘cat’, then the whole type is Cat. Since there’s only two options, then if it isn’t a Cat, it must be a dog.

Enums

A common use case you might see for union types is an alternative to enums. Let’s take a look at the following example:

async function fetchData(environment: string) {
    let path = '';
    if (environment === 'dev') path = 'localhost:8080';
    if (environment === 'production') path = 'upmostly.com/api/';
    if (environment === 'testing') path = 'localhost:8080/test';
    console.log('Path:', path);
    await fetch(path);
}

Just a simple function to fetch some data, using a different path depending on the environment. So let’s call our function:

fetchData('prod');

Oops, our function was expecting “production”, not “prod”.

It would be useful to have some way for TypeScript to tell us we made this mistake before we ran our code. Let’s use Union types to fix this:

type Environment = 'dev' | 'production' | 'testing';
async function fetchData(environment: Environment) {
    let path = '';
    if (environment === 'dev') path = 'localhost:8080';
    if (environment === 'production') path = 'upmostly.com/api/';
    if (environment === 'testing') path = 'localhost:8080/test';
    console.log('Path:', path);
    await fetch(path);
}

And immediately with these small changes, our error is revealed:

fetchData('prod'); //TS2345: Argument of type '"prod"' is not assignable to parameter of type 'Environment'.

Template Types

TypeScript offers you some powerful options for quickly creating union types.

Let’s imagine you want to build a colour palette, something like:

type Hue = 'light_red' | 'red' | 'dark_red' | 'light_orange' | 'orange' | 'dark_orange'//etc...

Simple right? We’ll just keep writing this out for every option. We could do this, or we could be smarter with template types. If you’re familiar with template literal strings in JavaScript, TypeScript lets you use the same syntax to build types. If you provide union types as parameters, TypeScript will build your union type with every combination of the union types:

type Hue = 'red' | 'green' | 'blue' | 'black' | 'white';
type Shade = 'light' | 'dark';
type Palette = `${Shade}_${Hue}` | Hue; 
//"light_red" | "light_green" | "light_blue" | "light_black" | "light_white" | "dark_red" | "dark_green" | "dark_blue" | "dark_black" | "dark_white" | "red" | ...

With just a few lines, we immediately get all the colour combinations we need.

Conclusion

Understanding and utilizing union types in TypeScript can greatly improve your code’s flexibility and readability. They enable developers to write more concise and robust code that can handle various data scenarios. So don’t be afraid to incorporate union types into your TypeScript projects, and enjoy the benefits they bring to your coding experience! Thanks for reading, and if you liked this article, feel free to leave a comment!

Avatar photo
👋 Hey, I'm Omari Thompson-Edwards
Hey, I'm Omari! I'm a full-stack developer from the UK. I'm currently looking for graduate and freelance software engineering roles, so if you liked this article, reach out on Twitter at @marile0n

💬 Leave a comment

Your email address will not be published. Required fields are marked *

We will never share your email with anyone else.