Flow Principles — Using Opaque Types

Tomas Barry
Butternut Engineering
4 min readApr 6, 2020

At Butternut Box, we have been using Flow, a static type checker for JavaScript, for close to two years. Static type checking gives us confidence that when we change code in one place we don’t break code in another place by ensuring that our types are consistent (we can’t give a string to a method that expects a number). In that time, we’ve uncovered some of the lesser-known features of Flow and been able to make use of them to deliver more robust software to our customers.

In this article, we’ll discuss one of those lesser-known features — Opaque Types. We will use a very basic example of email validation.

The problem

Imagine you have a method called sendEmail that given an email address, subject, and message sends an email. This method would look something like this:

// @flowconst sendEmail = (email: string, subject: string, message: string): void => {
// do something
}

Thinking a bit more about the type signature of the method, some type aliases would be pretty handy. We might rewrite the method like this:

// @flowtype Email = string
type Subject = string
type Message = string
const sendEmail = (email: Email, subject: Subject, message: Message): void => {
// do something
}

That is looking a bit better. Now Flow will be able to give us nicer error messages when we try to pass something other than a string as an argument to the sendEmail method.

However, we’ve glossed over one very crucial detail. We probably want to have some validation on the email address and maybe even some character limit on the subject and message.

For the sake of simplicity, let’s apply the following requirements on our sendEmail method:

  1. An email is valid if it contains an @ symbol (we could do a lot better, but let's just leave it at that)
  2. A subject must be between 5 and 20 characters long (inclusive)
  3. A message must be between 10 and 100 characters long (inclusive)

With these restrictions in mind, let’s look at a naive solution and then a solution that Flow enables.

The naive solution

// @flowtype Email = string
type Subject = string
type Message = string
const sendEmail = (email: Email, subject: Subject, message: Message): void => {
const isValidEmail = email.includes('@')
const isValidSubject = subject.length >= 5 && subject.length <= 20
const isValidEmail = message.length >= 10 && message.length <= 100

if (isValidEmail && isValidSubject && isValidEmail) {
// do something
}
}

This will work for what we need, but the sendEmail method is starting to get a bit bloated and we've only considered some very rudimentary validation. Now imagine how bloated the sendEmail method will become when we start adding more and more logic on top of the validation.

Wouldn’t it be nice if we could straight-up prevent a call to sendEmail unless the arguments met our criteria? Well, Flow can let us do this using Opaque Types.

Using opaque types

Opaque types are a feature of Flow that allows you to add restrictions to type aliases. Let’s look at how we could write an Opaque type for an email address:

// ValidEmail.js// @flowopaque type ValidEmail = string
type EmailCandidate = string
const toValidEmail = (candidate: EmailCandidate): ?ValidEmail => {
return candidate.includes('@')
? candidate
: null
}

Let’s now use our Opaque type:

// @flowimport { toValidEmail } from '/path/to/ValidEmail.js'import type {
ValidEmail
} from '/path/to/ValidEmail'
type Subject = string
type Message = string
const sendEmail = (email: ValidEmail, subject: Subject, message: Message): void => {
const isValidSubject = subject.length >= 5 && subject.length <= 20
const isValidEmail = message.length >= 10 && message.length <= 100

if (isValidSubject && isValidEmail) {
// do something
}
}
const validEmail: ?ValidEmail = toValidEmail('good@email.com')if (validEmail) {
// Now we know for certain that validEmail passes our
// email validation and the call to sendEmail will not
// fail on us
sendEmail(validEmail, 'subject', 'some message')
}

So what have we changed and why is it helpful?

Well, we have updated our sendEmail method to accept as a first argument a value of type ValidEmail. This is different from the type alias that we had previously defined as a ValidEmail can only be obtained by calling the toValidEmail method whereas the type alias was just an alias for a string.

We have been able to delegate the complexity of email address validation to the toValidEmail method and we have ensured now that the email address in any call to sendEmail will be valid according to our definition. This is really useful as our testing of sendEmail does not have to consider testing sendEmail with an invalid email address. We have been able to constrain the scope of possible values to thesendEmail method to only those that are valid and decouple the complexity of having to validate the email.

Conclusion

We have been able to apply further type safety to our applications through the use of opaque types. They are a really powerful feature that I highly recommending looking into and using as part of your JavaScript applications.

At the very least, I hope that you have found out about a Flow feature that you perhaps were not aware of before reading this article.

I’ll leave it to you to create an opaque type for a Subject and a Message.

--

--