You need some sort of a tag if you want to pattern match at runtime, which is the usual use case for tagged unions. Now, a tagged union like
type Option a = Some a | None # here Some and None are tags, not types themselves
could also be written as an
untagged union by using types as tags:
type Some a # a wrapper type for an a
type None # a unit type
type Option a = Some a | None
but for that to be useful you still need to be able to check at runtime whether you have a Some or a None… which means that the tags
are still there, just hidden by the language.
But the more typical use case for untagged unions is different: given a type such as
type StringOrInt = String | Int
you're not even supposed to be able to check whether you in fact have a String or an Int, rather the language only allows you to use the subset of operations defined for
both String and Int. In other words, slightly ironically, the type StringOrInt exposes the
intersection of String API and Int API.