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 = "[email protected]"
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 String
s, 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)
else
Nothing
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.