A union type in programming can be one type from any given types. TypeScript allows combining multiple types into a single type. Let's see what a union type in TypeScript looks like.

type StringOrNum = string | number;

const a: StringOrNum = 1;
const b: StringOrNum = "one";

console.log(a);	// 1
console.log(b); // 'one'
Union of built-in types in TypeScript

The above example demonstrates how the type StringOrNum can be of type string or type number at any instance. It is because StringOrNum is a union type of string and number type. In the case of mutable variables, the types can also be interchanged.

let c: StringOrNum = "2";
console.log(c); // 2

c = "two"
console.log(c) // "two"
Union type holding variables of multiple types in a union at an instance

The Behaviour of Union Type in TypeScript

Unions are not limited to built-in types. They can also be used in user-defined types and interfaces as well. Thy syntax for doing so is not so different to normal union.

type Circle = { radius: number };

interface Square {
  side: number;
};

type Shape = Circle | Square;

const shape1: Shape = {
  radius: 10,
};

console.log(shape1); // { "radius": 10 } 
Union type between an Interface and Type

Here we can see that the type Shape can be either Circle or a Square. We also made a variable of Shape type with radius property. By looking at the structure of the object, we can tell that shape1 is of type Circle.

However, we have a slight problem. The union type Circle | Square is assignable to either Circle or Square, so it must have properties from Circle or Square (or both).

In the above context, the following code would also be valid.

cosnt shape2: Shape = {
  side: 20
};

const shape3: Shape = {
  radius: 10,
  side: 20
};

console.log(shape2);	// { "side": 10 } 
console.log(shape3);	// { "radius": 10, "side": 20 }
Actual behaviour of union of custom types

Tagged Unions to the Rescue!

With Shape type in the last example, we saw how the union actually works with custom types in the case of TypeScript. But since TypeScript v2.0, we can use tagged unions, also known as discriminated unions.

To create a tagged union, each type of union must have a tag or a discriminator property. I will use the 'type' property as a discriminator, but it can be anything.

Let us rewrite the above examples with shapes using tagged union.

type Circle = {
  type: "Circle",
  radius: number
}

interface Square {
  type: "Square",
  side: number
}

type Shape = Circle | Square;
Defining a tagged union between Circle and Square types

We redefined our types with an extra property, 'type', which defies what the type actually is. Now let's see the magic.

const shape1: Shape = {
  type: "Square",
  radius: 10
}
Invalid case with tagged union

The above code snippet actually gives us an error now.

Since we provided the discriminator value of Square, the TypeScript compiler now knows that we are trying to create an instance of the type Square . And since the type Square doesn't have a property, radius – the compiler, complains so. Pretty cool, right?

We even get added benefit from the compiler suggesting the discriminator and the correct properties for the given discriminator, as shown below:

LSP provides auto-completion for the discriminator
LSP suggests the properties of type Circle

Use Cases of Tagged Unions

Let's say we want a function that calculates the area of a Shape. Without tagged unions, it would be messy with a lot of explicit casting. But with tagged unions, it's a lot simpler. Let us see how.

function getArea(shape: Shape): number {
  if(shape.type === "Circle")
    return Math.PI * shape.radius * shape.radius;

  if(shape.type === "Square")
    return shape.side * shape.side;

  return 0;
}


const shape1: Shape = {
  type: "Circle",
  radius: 10
};

const shape2: Shape = {
  type: "Square",
  side: 20
};

console.log(getArea(shape1));	// 314.1592653589793 
console.log(getArea(shape2));	// 400
Simple use case of tagged union

Here, we used the discriminator to distinguish what a shape can be and process the data accordingly.

The TypeScript compiler knows what shape can be; it even looks at the condition statement. It then infers the correct type for the object, as we can see from the if statements above.

While checking if the type is Circle, the compiler knows that shape is now only of the type Circle and infers that type to shape.

ℹ️
Guess what?! The compiler can even provide the correct type definition. It noticed that we had already checked the Circle type, and only provides us with the Square option.
Auto suggestion for conditional type checking

We can also use the switch case and achieve the same result.

function getArea(shape: Shape): number {
  switch(shape.type) {
    case "Circle":
      return Math.PI * shape.radius * shape.radius;
    case "Square":
      return shape.side * shape.side;
    default:
      return 0
  }
}
Rewriting getArea with switch-case instead of the if-else ladder

Like in the if statements, the discriminator value suggestions are also provided using switch-case syntax.

Suggestions for type discriminator in switch-case syntax

Conclusion

Tagged union is a powerful tool in TypeScript that allows users to create polymorphic types with proper type inference. Its use case can span simple polymorphic functions to polymorphic components in a UI library like React with proper type inference.

Thank you for reading this article. Consider leaving a comment and subscribing if you liked it.