TypeScript: More on Union Types

In the previous article, I explained the basics of union types in TypeScript.

Though union types resemble sum types of algebraic data types, they are different types. (Confusingly, sum types are sometimes also called “unions” or “disjoint unions” in some languages.). The notion of union types, A|B denotes the ordinary union of the set of values belonging to A and the set of values belonging to B, with no added tag to identify the origin of a given element.

With this in mind, let’s look at some properties of TypeScript union types:

  • Identity: A|A is equivalent to A
  • Commutativity: A|B is equivalent to B|A
  • Associativity: (A|B)|C is equivalent to A|(B|C)
  • Subtype collapsing: A|B is equivalent to A if B is a subtype of A

In sum types, A|A is different from A because it is the sum of type A and A. (either left A or right A). However, in union types, A|A is equivalent to A because there is no tag attached to make left and right ‘A’ distinct. So it is also called “non-disjoint union types” to differentiate it from “disjoint union types” of ADT.

Non-disjoint union types are different from disjoint union types in that it lacks any kind of case construct. If v is of type A|B, the only members that we can safely access on v are members that belong to both A and B. This is why we can access only weight and gears members of transport object.

interface Car {
    weight: number;
    gears: number;
    type: string;
}
interface Bicycle {
    weight: number;
    gears: boolean;
    size: string;
}
var transport: Car|Bicycle = /* ... */;
console.log(transport.type); // Error, transport does not have 'type' property

So if the constituent types of a union type have no members in common, there is nothing we can do on the union type. For example, if x is of number|string|boolean type, we can only access toString and valueOf members that are common across these three types.

Wait a minute! I’ve just mentioned that union types lack any kind of case construct. But what about the type guards of TypeScript? Isn’t it a kind of case construct that can identify the origin of an element? The answer is both yes and no. Yes, you can inspect the runtime type of a union type with type guards. But no because there is nothing special that prevents you from checking non-constituent types. For example, the following code is perfectly okay in TypeScript:

var x: number | boolean;
if (typeof x === 'number') {
    // ...
}

x is either number or boolean, but the type guard here checks if the type of x is number. In the body of if expression, TypeScript infers that the type of x is number though it never can’t be of number type. On the contrary, in programming languages with sum types, we can check only the constituent types with pattern matching.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s