Hiding Implementation Details With Flow’s New Opaque Type Aliases Feature

Do you ever wish that you could hide your implementation details away from your users?

Well, now all of your dreams have finally come true! Flow 0.51.0 added support for opaque type aliases, with babel support coming in the next week or so. Opaque type aliases are type aliases that hide their underlying type. You can only see an opaque type’s underlying type in the file which declares the opaque type. They’re already documented here, so we’ll spend the rest of this blog post showing just how powerful opaque type aliases can be.

Maintaining Invariants with Opaque Types

Opaque type aliases are really useful for maintaining invariants in your code. Whenever you find yourself wanting to express “things of type T where X is true,” you might want to consider using an opaque type alias.

As a simple example, lets consider a type for non-negative numbers:

NonNeg.js:
// @flow
opaque type NonNeg = number;

Now we can make some functions to interact with NonNeg numbers:

NonNeg.js:
// @flow
opaque type NonNeg = number;
export function add(x: NonNeg, y: NonNeg): NonNeg {
return x + y;
}
// Returns 0 at minimum
export function subtract(x: NonNeg, y: NonNeg): NonNeg {
return Math.max(0, x - y);
}
export function zero(): NonNeg {
return 0;
}
export function increment(x: NonNeg): NonNeg {
return x + 1;
}

Making an opaque type alias is like making a contract with your user: if they agree to only use your library to interact with your type, you will maintain any invariant your type guarantees.

Your users can now go forth and use your type, always knowing it won’t be a negative number:

imports.js:
// @flow
import {add, subtract, zero} from './NonNeg';
const w = zero();
const x = increment(w);
const y = add(w, x);
const z = subtract(y, w);

Now suppose one of our users tries to break our invariant:

bad-usage.js:
// @flow
import {zero} from './NonNeg';
let a = zero();
let b: NonNeg = a - 1;

Since we used an opaque type alias, which doesn’t expose its type outside of its defining file, they’ll get an error:

Error: a = a - 1;
^ Error: a is NonNeg, which is incompatible with number.

Awesome! We were able to use Flow to protect our NonNeg invariant! But this might be a bit too restrictive. After all, a non-negative number is still a number, so we should be able to use it as such.

Subtyping Constraints

We can add a subtyping constraint to our opaque type alias to express that it can still be used as a number outside of the file.

NonNeg.js:
// @flow
opaque type NonNeg: number = number;
/* ... */

Adding : number to our opaque type alias tells Flow that every NonNeg is a number. This does not imply, however, that every number is a NonNeg.

Now let’s take another look at the client code. There is still an error!

let b: NonNeg = a - 1;
^ Error: number. This type is incompatible with
let b: NonNeg = a - 1;
^ opaque type `NonNeg`.

Notice that now Flow interprets a — 1 as a number, but won’t allow it to be assigned to a variable that is a NonNeg. If we wanted to use the value of a as a number, we’ll have to use it in a place that expects a number.

let b: number = a - 1; // Ok!

To continue the metaphor, this is the user breaking your contract. As soon as they stop using your library functions, you no longer guarantee that the number is non-negative.

Some more use cases

  • Are you using strings to represent unique IDs in your application? Not all strings are IDs — you can use an opaque type alias to make sure that no random strings get passed around pretending to be IDs!
  • Do you have two type aliases with identical underlying types but completely different uses? If you make them opaque, you won’t be able to substitute one for the other!
  • Do you carefully maintain complex invariants in your data structure and want to protect users from ruining them? Guard it with an opaque type alias!

Conclusion

Opaque types in Flow are a contract you make with the users of your type. The contract can be eternally binding, where you user has no choice but to use your functions. Alternatively, you can provide a subtyping escape hatch to your user, allowing the user to break the contract whenever they please.


Are you building cool things with Flow or opaque type aliases? We want to hear from you! Send a tweet to flowtype on Twitter so the whole community may celebrate your efforts to make writing JavaScript delightful.