Union types are everywhere in Elm, but when you’re starting to learn the language, they can be a bit unfamiliar and confusing, because they don’t really exist in other languages like JavaScript or C#.
The main question I’ve seen people ask is: how do I extract a value from a union type?
Suppose you have a union type like this:
type UserId
= Id Int
| Uuid String
A UserId
value can contain either an Int
or a String
. The solution for getting to those values is simple because Elm provides only one tool for that - the case
expression:
getUser : UserId -> User
getUser userId =
case userId of
Id id ->
getUserById id
Uuid id ->
getUserByUuid id
Given a userId
of type UserId
, we need to write a case
expression which accounts for every possible tag in the union type, and in each of those branches we can name the contained value (I called it id
in both branches) and then use it. In my case, I pass the id
to these functions:
getUserById : Int -> User
getUserByUuid : String -> User
So, whenever you want to get the value in a variable of a union type, you need to write a case
expression.
Another common pattern in Elm is to define new types like this:
type UserName = UserName String
This is different from defining a type alias and is a useful technique which I discuss in my upcoming book.
The interesting thing about it is that it’s not another language construct but rather a special case of union types. It’s just a regular old union type that only contains one tag - in this case UserName
, the same as the name of the type itself, which is totally cool in Elm.
How do we extract the string from a value of type UserName
? Well, we can use a case
expression like we did before:
getName : UserName -> String
getName userName =
case userName of
UserName s ->
s
getName <| UserName "Joe" -- returns "Joe"
However, this does look a bit silly, so in this situation, Elm provides a convenient shortcut. We can write getName
like this instead:
getName (UserName s) = s
getName <| UserName "Joe" -- returns "Joe"
Here, we are using destructuring directly in the argument list of the function in order to name the contained string.
In case you are wondering, if you try to do this when the union type has multiple tags, the compiler will complain.
Destructuring works in let
expressions as well:
toUpper : UserName -> String
toUpper userName =
let
(UserName s) = userName
in
String.toUpper s
Finally, it works in anonymous functions too (there is very little difference between anonymous and named functions):
(\(UserName s) -> s) (UserName "Joe") -- returns "Joe"
There are a lot more uses for destructuring, and it’s something I also talk about in great detail in my upcoming book.
In summary, if you have a union type with multiple tags, then you need to use a case
expression to extract the contained values. If you have a single tag union type, then you can use destructuring as a shortcut when defining functions or writing let
expressions.