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.