Altcademy - a Forbes magazine logo Best Coding Bootcamp 2023

What are Unions in TypeScript?

Unions in TypeScript are a powerful and versatile feature that allows you to work with multiple types at once. In this post, we'll explore what unions are, how to use them, and some practical examples of how they can make your TypeScript code more flexible and robust.

What are Unions?

In the world of programming, we often come across situations where a variable, function, or object may have more than one type. For example, a function might accept a string or a number as its input, or an object might have properties whose values can be of different types.

TypeScript, being a statically-typed language, requires you to specify the types of your variables, function parameters, and object properties. This can sometimes feel restrictive, especially when you're trying to model complex, real-world scenarios.

This is where unions come into play. Unions are a way to declare that a value can belong to one of several possible types. In other words, a union type is a type that represents a value that can be of more than one type. This allows you to model complex scenarios in a more flexible way while still benefiting from TypeScript's type checking.

How to Use Unions

To create a union type, you simply list the possible types separated by a vertical bar (|). For example, if you have a variable that can be either a string or a number, you can declare its type as follows:

let myVariable: string | number;

Now, myVariable can be assigned a value of either string or number type:

myVariable = "hello";
myVariable = 42;

If you try to assign a value of any other type to myVariable, TypeScript will give you a type error:

myVariable = true; // Error: Type 'boolean' is not assignable to type 'string | number'.

You can also use union types for function parameters, return types, and object properties. Here's an example of a function that takes a string or a number as input and returns a string or an Array<number>:

function processInput(input: string | number): string | Array<number> {
  if (typeof input === 'string') {
    return input.toUpperCase();
  } else {
    return [input, input * 2];
  }
}

const result1 = processInput('hello'); // "HELLO"
const result2 = processInput(42); // [42, 84]

In this example, we use the typeof operator to determine the type of the input at runtime and perform different actions based on its type. Notice how TypeScript can infer the type of the return value based on the input type, making it easy to work with union types in your code.

Practical Examples of Using Unions

Now that we've covered the basics of unions, let's take a look at some real-world examples of how they can be used to make your TypeScript code more flexible and robust.

Handling Different Input Types

As we saw in the previous example, unions can be used to create functions that accept different types of input:

function stringify(value: string | number | boolean): string {
  if (typeof value === 'string') {
    return value;
  } else if (typeof value === 'number') {
    return value.toString();
  } else {
    return value ? 'true' : 'false';
  }
}

console.log(stringify('hello')); // "hello"
console.log(stringify(42)); // "42"
console.log(stringify(true)); // "true"

In this example, the stringify function accepts a value of type string, number, or boolean and returns a string representation of the value. By using a union type for the input parameter, we can create a more flexible and reusable function.

Modeling Complex Data Structures

Unions can also be used to model complex data structures that have properties with different types. Consider the following example of a Person object that has a name property (a string) and an age property (a number):

type Person = {
  name: string;
  age: number;
};

Now, imagine you want to extend the Person object to support an optional email property that can be either a string or an array of string values (to represent multiple email addresses). You can use a union type to achieve this:

type PersonWithEmail = {
  name: string;
  age: number;
  email?: string | Array<string>;
};

const person1: PersonWithEmail = {
  name: 'Alice',
  age: 30,
  email: 'alice@example.com',
};

const person2: PersonWithEmail = {
  name: 'Bob',
  age: 25,
  email: ['bob@example.com', 'bob@gmail.com'],
};

In this example, the PersonWithEmail type extends the Person type with an optional email property that can be either a string or an array of string values. This allows you to model more complex scenarios while still benefiting from TypeScript's type checking.

Working with Discriminated Unions

Discriminated unions, also known as tagged unions or algebraic data types, are a more advanced form of unions that allow you to create complex types with shared properties. In a discriminated union, each type in the union has a common property (usually called a "tag" or "discriminator") that allows you to uniquely identify the type at runtime.

Here's an example of a discriminated union that represents different types of shapes:

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

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

type Rectangle = {
  kind: 'rectangle';
  width: number;
  height: number;
};

type Shape = Circle | Square | Rectangle;

In this example, the Shape type is a union of Circle, Square, and Rectangle types, each of which has a unique kind property that acts as a discriminator. This allows you to create functions that can work with different types of shapes and perform different actions based on their kind property:

function getArea(shape: Shape): number {
  switch (shape.kind) {
    case 'circle':
      return Math.PI * shape.radius * shape.radius;
    case 'square':
      return shape.sideLength * shape.sideLength;
    case 'rectangle':
      return shape.width * shape.height;
    default:
      // This ensures that we've handled all possible shape types
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

const circle: Circle = { kind: 'circle', radius: 5 };
const square: Square = { kind: 'square', sideLength: 4 };
const rectangle: Rectangle = { kind: 'rectangle', width: 3, height: 2 };

console.log(getArea(circle)); // 78.53981633974483
console.log(getArea(square)); // 16
console.log(getArea(rectangle)); // 6

In this example, the getArea function uses a switch statement to determine the type of the shape parameter at runtime and calculate its area accordingly. By using a discriminated union, we can create more flexible and type-safe code that can handle complex scenarios.

Conclusion

Unions in TypeScript are a powerful and versatile feature that allows you to work with multiple types at once, making it easier to model complex, real-world scenarios. By using union types, you can create more flexible and robust TypeScript code that can handle different input types, model complex data structures, and work with discriminated unions.

As you continue to learn and work with TypeScript, you'll likely find more and more situations where unions can help you create cleaner, safer, and more maintainable code. So go ahead and embrace the power of unions in your TypeScript projects!