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 = "firstname.lastname@example.org" 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
type rather than a
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) = ...
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
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
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
If you'd like to export the
module Email exposing (Email(EmailAddress), create, send)
Now any file that imports
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.