This is part of a short series of Flow/TypeScript posts I’m calling “Effective Types”. Posts so far:

Type Wrappers are a way of taking an existing type (like strings, numbers, etc) and wrapping them in another type. This is so that:

  1. They can’t easily be confused (eg. getting two strings getting mixed up), and
  2. So we can represent things like validation in a way that Flow can check.

They are variously known as Nominal Type Aliases, Opaque Type Aliases, or Newtypes1.

Let’s get to it. 😊

Creating and Using a Type Wrapper

I have a library called flow-classy-type-wrapper; towards the end of this post, we’ll get into the guts of how this works, but let’s arm-wave for the moment. Here’s an example of it in use:

import { TypeWrapper } from 'flow-classy-type-wrapper';

class Email extends TypeWrapper<string> {}

function validateEmail(x: string): (Email | null) {
  if (x.match(/@/)) {
    return Email.wrap(x);
  }
  return null;
}

function sendEmail(to: Email) {
  const toAsString = Email.unwrap(to);
  // A stand-in for sending an email:
  console.log("Sending to " + toAsString + "...");
}

const input = "hello@world.com";
const email = validateEmail(input);

if (email === null) {
  throw new Error("Was not email: " + input);
}
sendEmail(email);

Let’s break this down into smaller pieces:

class Email extends TypeWrapper<string> {}

This extends the imported TypeWrapper class, putting string in as its type parameter. If this looks unfamiliar to you, I recommend you learn about these parameters before continuing.

In any case: we have a class representing a new type, Email, that wraps another, string. It has two static methods attached to it, wrap and unwrap. We’ll use them in a moment.

function validateEmail(x: string): (Email | null) {
  if (x.match(/@/)) {
    return Email.wrap(x);
  }
  return null;
}

This is a function that accepts a string, and spits out either an Email, or null, depending whether it passes our rather flimsy valid-email check.

To get an Email value, we use the aforementioned wrap method; its type signature, for our example, looks like function wrap(x: string): Email. We use this wrap method to “wrap up” our string value so that it can’t be directly used as a string anymore; the only thing that can make use of this wrapped-up value are things that expect Email specifically, which we’ll see an example of below:

function sendEmail(to: Email) {
  const toAsString = Email.unwrap(to);
  console.log("Sending to " + toAsString + "..."); // <-- A stand-in for sending an email.
}

This function accepts an Email, and then unwraps it to get at the string which, in our toy example, just gets printed out. Much like wrap above, this unwrap function has a type signature that looks like function unwrap(x: Email): string.

Lastly:

const input = "hello@world.com";
const email = validateEmail(input);

if (email === null) {
  throw new Error("Was not email: " + input);
}
sendEmail(email);

We’re pretending to accept user input here (a string). With that input, we put it through our validation function, and are made to (by Flow) handle the possibility of the result being null. After we’ve checked that, we know we have an Email value. We pass it on to sendEmail to use.

What is Wrapping Good For?

In-line Documentation

The simplest benefit: it’s a way of giving names to values of a particular variety, and goes a little way towards providing some documentation that’s also checked by Flow.

On a visual level I’d personally say that a function with a signature sendEmail(from: Email, to: Email, subject: string) is easier to read than sendEmail(to: string, from: string, subject: string). We also get the benefits of being able to look up what describes or produces an Email specifically, instead of any old string.

Preventing Mixups

Say we have a function that takes two strings, and returns a potential user:

function createUser(email: string, password: string): Promise<User> {
  // ...
}

// Just validates and returns a string or null:
function validateEmail(email: string): (string | null) {
  // ...
}

// Much later:

// Let's pretend we got this email+password from user input.
const email = validateEmail("bob@example.com");
const password = "canhefixit";
if (email !== null) {
  const bob = createUser(email, password);
}

There’s the possibility of getting those strings mixed up, eg. createUser(password, email). It’s unlikely in such a simple case, but with more complex ones like fs.writeFile(string, string, string) or fs.writeSync(handle, buffer, number, number, number) (both from the Node.js standard library), you can see how things can get mixed up if you have a bunch of arguments of the same type.

And yep, you could have them as part of an object with email and password fields instead, eg. { email: string, password: string }. That helps not mix things up, but the wrapped version of the value is still safe even when separated from that object:

const args: { email: string, password: string } =
  { email: "bob@example.com", password: "abc" };

const email = args.email;
// ... The type is now just string; we've lost the context.
const args: { email: Email, password: string } =
  { email: Email.wrap("bob@example.com"), password: "abc" };

const email = args.email;
// ... The type is still Email.

Changing Code

We might need to change createUser to take a username instead, eg. shifting email off to something a user can add later after signup:

function createUser(username: string, password: string): Promise<User> {
  // Changing this: ^^^^^^^^
}

function validateEmail(email: string): (string | null) {
  // ...
}

// Much later:
const email = validateEmail("bob@example.com");
const password = "canhefixit";
if (email !== null) {
  const bob = createUser(email, password);
}

Uh oh. bob@example.com isn’t a valid username; suddenly this user creation step fails. Hopefully, if we have tests for this path through the code, we might have caught this. But what if we use a typed wrapper to make it so that Flow tells us immediately if we change something like this?

// ---- Before change: takes an Email ----
function createUser(email: Email, password: string): Promise<User> {
  // ...
}

function validateEmail(email: string): (Email | null) {
  // ...
}

// Much later:
const email = validateEmail("bob@example.com"); // type: Email | null
const password = "canhefixit";
if (email !== null) {
  const bob = createUser(email, password);
}
// ----- After change: takes a Username -----
function createUser(username: Username, password: string): Promise<User> {
  // ...
}

// Much later:
const email = validateEmail("bob@example.com"); // type: Email | null
const password = "canhefixit";
if (email !== null) {
  const bob = createUser(email, password);
  // 💥 Boom! 💥
  // createUser(email, password);
  //            ^^^^^ Email. This type is incompatible with
  //                  the expected param type of
  // function createUser(username: Username, password: string): Promise<User> {
  //                               ^^^^^^^^ Username
}

“Gatekeeper” Functions

Lets look at our validateEmail again from earlier:

function validateEmail(x: string): (Email | null) {
  // ...
}

We have a function that produces Email. What if, to make an Email, we had to put the input through this validateEmail function?

// ----- email.js -----
import { TypeWrapper } from 'flow-classy-type-wrapper';

// Set up our Email type-wrapper around string, but don't export it, instead...
class Email extends TypeWrapper<string> {}

// ... Export only the type, for use in type signatures.
export type EmailType = Email;

// Export the unwrap function.
export const unwrap = Email.unwrap;

// Export the validateEmail function.
export function validateEmail(x: string): (Email | null) {
  // ...
}
// ----- stuff.js -----
// We can only import the type, email validation, and email unwrapping; we
// can't create our own Email anymore.
import type { EmailType as Email } from './email';
import { validateEmail, unwrap }

const email = validateEmail("bob@example.com");
if (email === null) throw new Error("Invalid email");

// We know we have an Email now; we could give it to createUser() here, or pass
// it to somewhere else deep in our program, knowing that it's a Email.
createUser(email, "canhefixit");

We only export validateEmail, unwrap and the Email type itself. In doing so, make sure that any Email values being tossed around our app have been passed through the validateEmail function; there’s no other way to make one without using an any backdoor, and we can use eslint to warn whenever we try to use these.

For me personally, this guarantee is important. The more guarantees I have about things when I’m writing code (eg. in this case, any Email I have is always a validated value), I find the more focused I can be on a piece of work; the more “local” I can think.

A Sliding Scale of Safety

As we’ve just done, you can ratchet up the safety, at the expense of restricting how you construct and use these values. I personally use wrapped types as much as practical, them having saved my butt more than a few times (eg. where a “string” has been passed through a few tiers of functions, to be accidentally mixed up somewhere else). But feel free to use this with some values you want to carefully control, like User IDs, escaped strings, etc.

So what are the “Email” Values?

(And what is the performance overhead for this?)

In the case of Email: just strings! “Wrapping” the values never transforms them. The idea of Email being a separate thing is entirely for Flow’s benefit, and pretty much evaporates when the program is run.

Let’s look at TypeWrapper’s definition:2

export class TypeWrapper<Inner> {
  // A constructor that can't be called (nothing can satisfy the "empty" type).
  constructor(_: empty): void {}

  // Inner Value -> Wrapped Value
  static wrap(x: Inner): this {
    return (x: any); // eslint-disable-line flowtype/no-weak-types
  }

  // Wrapped Value -> Inner Value
  static unwrap(x: this): Inner {
    return (x: any); // eslint-disable-line flowtype/no-weak-types
  }
}

It’s a class that, when extended, provides a class with some static helper methods. It could be a type and a couple of functions, but this way lets you define a wrapper with an class Foo extends TypeWrapper<Bar> one-liner.

If you fill in the type-variable gaps, you could think of it like the following (assuming our earlier Email-wrapping-string example):

export class Email {
  constructor(_: empty): void {}

  // string -> Email
  static wrap(x: string): Email {
    return (x: any);
    // ^-- x is forced to "any", then back to "Email" by the return type.
  }

  // Email -> string
  static unwrap(x: Email): string {
    return (x: any);
    // ^-- Same, except to "string".
  }
}

All we’re doing with the Email.wrap and Email.unwrap methods are just casting between two values. The underlying value never changes.

There is overhead, but it’s minimal:

  • If you’re transpiling to ES5 or below, every class definition will be converted to its ES5 equivalent. A one-time cost for class, plus about seven unminified lines of Babel output per extends TypeWrapper.
  • Every time you wrap or unwrap a value, it goes through a function that looks like:
    function(x) { return x; }
    

    … Which sounds wasteful, but in this performance test, the difference doesn’t even seem to clear the margin of error.

Further Examples

I’ll leave you now with some examples of Type Wrappers in action. I’m interested to hear what other uses you come up with. 😊

// Something representing a positive number; a small set of
// the all the possible number values, without having to
// list them all out.
// (This example is pretty similar to Email above.)
class PositiveNumber extends TypeWrapper<number> {}

function toPositive(n: number): (PositiveNumber | null) {
  if (n >= 0) {
    return PositiveNumber.wrap(n);
  }
  return null;
}

// Some helper I just made up that can use this:
function repeat<T>(item: T, times: PositiveNumber): Array<T> {
  return Array(PositiveNumber.unwrap(times)).fill(item);
}

const times = toPositive(5);
if (times !== null) {
  const fiveNines = repeat(9, times);
  console.log(fiveNines); // [9,9,9,9,9]
}
// Let's get trickier. :)
// This is a type representing an Array of T, guaranteed
// to have at least one element:
class NonEmptyArray<T> extends TypeWrapper<Array<T>> {}

// eg. number, Array<number> -> NonEmptyArray<number>
function toNonEmptyArray<T>(
  first: T,
  rest: Array<T>
): NonEmptyArray<T> {
  const list = [first].concat(rest);
  return NonEmptyArray.wrap(list);
}

const nemp = toNonEmptyArray(1, [2,3,4]);
const list = NonEmptyArray.unwrap(nemp);
console.log(list); // [1,2,3,4]
// This uses something called "Phantom Types", the subject of my next post.
// We have Temperature wrapping a number, but then has
// Temperature carry some extra information (eg. Celsius)
// that only exists in the types ("at type-level").
class Temperature<T> extends TypeWrapper<number> {}
class Fahrenheit { }
class Celsius { }

function createF(n: number): Temperature<Fahrenheit> {
  return Temperature.wrap(n);
}

function convertFtoC(
  f: Temperature<Fahrenheit>
): Temperature<Celsius> {
  const n = Temperature.unwrap(f);
  return Temperature.wrap((n - 32) / 1.8);
}

const boiling = convertFtoC(createF(212));
console.log(Temperature.unwrap(boiling)); // => 100

1. I’ve intentionally avoided calling this Flow technique “newtyping”, mostly because Haskell’s newtype lets you do things that we can’t with this, eg. newtype Foo a = Foo (a -> String), where it’s not a straight “value wrapping”.

2. This class+statics definition idea was inspired by @mkscrg’s original “class bundle” pattern from this Flow issue. Cheers!