Now let's say that we have the following simple business rule: "A contact must have an email or a postal address". Does our type conform to this rule?

The answer is no. The business rule implies that a contact might have an email address but no postal address, or vice versa. But as it stands, our type requires that a contact must always have both pieces of information.

But now we have gone too far the other way. In this design, it would be possible for a contact to have neither type of address at all. But the business rule says that at least one piece of information must be present.

What's the solution?

Making illegal states unrepresentable

If we think about the business rule carefully, we realize that there are three possibilities:

A contact only has an email address

A contact only has a postal address

A contact has both a email address and a postal address

Once it is put like this, the solution becomes obvious -- use a union type with a case for each possibility.

This design meets the requirements perfectly. All three cases are explictly represented, and the fourth possible case (with no email or postal address at all) is not allowed.

Note that for the "email and post" case, I just used a tuple type for now. It's perfectly adequate for what we need.

Constructing a ContactInfo

Now let's see how we might use this in practice. We'll start by creating a new contact:

letcontactFromEmailnameemailStr=letemailOpt=EmailAddress.createemailStr// handle cases when email is valid or invalidmatchemailOptwith|Someemail->letemailContactInfo={EmailAddress=email;IsEmailVerified=false}letcontactInfo=EmailOnlyemailContactInfoSome{Name=name;ContactInfo=contactInfo}|None->Noneletname={FirstName="A";MiddleInitial=None;LastName="Smith"}letcontactOpt=contactFromEmailname"abc@example.com"

In this code, we have created a simple helper function contactFromEmail to create a new contact by passing in a name and email.
However, the email might not be valid, so the function has to handle both cases, which it doesn't by returning a Contact option, not a Contact

Updating a ContactInfo

Now if we need to add a postal address to an existing ContactInfo, we have no choice but to handle all three possible cases:

If a contact previously only had an email address, it now has both an email address and a postal address, so return a contact using the EmailAndPost case.

If a contact previously only had a postal address, return a contact using the PostOnly case, replacing the existing address.

If a contact previously had both an email address and a postal address, return a contact with using the EmailAndPost case, replacing the existing address.

So here's a helper method that updates the postal address. You can see how it explicitly handles each case.

letcontact=contactOpt.Value// see warning about option.Value belowletnewPostalAddress=letstate=StateCode.create"CA"letzip=ZipCode.create"97210"{Address={Address1="123 Main";Address2="";City="Beverly Hills";State=state.Value;// see warning about option.Value belowZip=zip.Value;// see warning about option.Value below};IsAddressValid=false}letnewContact=updatePostalAddresscontactnewPostalAddress

WARNING: I am using option.Value to extract the contents of an option in this code.
This is ok when playing around interactively but is extremely bad practice in production code! You should always use matching to handle both cases of an option.

Why bother to make these complicated types?

At this point, you might be saying that we have made things unnecessarily complicated. I would answer with these points:

First, the business logic is complicated. There is no easy way to avoid it. If your code is not this complicated, you are not handling all the cases properly.

Second, if the logic is represented by types, it is automatically self documenting. You can look at the union cases below and immediate see what the business rule is. You do not have to spend any time trying to analyze any other code.