Elm Language Improving Type-Safety Using New Types


Aliasing types cuts down on boilerplate and enhances readability, but it is no more type-safe than the aliased type itself is. Consider the following:

type alias Email = String

type alias Name = String

someEmail = "holmes@private.com"

someName = "Benedict"

sendEmail : Email -> Cmd msg
sendEmail email = ...

Using the above code, we can write sendEmail someName, and it will compile, even though it really shouldn't, because despite names and emails both being Strings, they are entirely different things.

We can truly distinguish one String from another String on the type-level by creating a new type. Here's an example that rewrites Email as a type rather than a type alias:

module Email exposing (Email, create, send)

type Email = EmailAddress String

isValid : String -> Bool
isValid email = 
  -- ...validation logic

create : String -> Maybe Email
create email =
    if isValid email then
        Just (EmailAddress email)

send : Email -> Cmd msg
send (EmailAddress email) = ...

Our isValid function does something to determine if a string is a valid email address. The create function checks if a given String is a valid email, returning a Maybe-wrapped Email to ensure that we only return validated addresses. While we can sidestep the validation check by constructing an Email directly by writing EmailAddress "somestring", if our module declaration doesn't expose the EmailAddress constructor, as show here

module Email exposing (Email, create, send)

then no other module will have access to the EmailAddress constructor, though they can still use the Email type in annotations. The only way to build a new Email outside of this module is by using the create function it provides, and that function ensures that it will only return valid email addresses in the first place. Hence, this API automatically guides the user down the correct path via its type safety: send only works with values constructed by create, which performs a validation, and enforces handling of invalid emails since it returns a Maybe Email.

If you'd like to export the Email constructor, you could write

module Email exposing (Email(EmailAddress), create, send)

Now any file that imports Email can also import its constructor. In this case, doing so would allow users to sidestep validation and send invalid emails, but you're not always building an API like this, so exporting constructors can be useful. With a type that has several constructors, you may also only want to export some of them.