Emulating TypeScript union types with ReasonML
Tags: reason, ocaml, bucklescript
Published: 2020-04-08 00:34:42

---
title: Emulating TypeScript union types with ReasonML
published: true
description: 
tags: reason,ocaml,bucklescript
---

YESTERDAY Gary Bernhardt posted [a compelling write-up](https://www.executeprogram.com/blog/porting-a-react-frontend-to-typescript) about the benefits they got from converting a JavaScript application to TypeScript. One of the really cool examples in that post is how TypeScript can automatically generate a union type for you by examining a JavaScript object literal during typechecking. That got me to thinking about how ReasonML/OCaml might be able to do the same thing.

## The TypeScript version

Here's a simplified version of the TypeScript example:

```typescript
const icons = {
  arrowDown: {label: 'Down Arrow'},
  arrowLeft: {label: 'Left Arrow'},
}

type IconName = keyof typeof icons

function iconLabel(iconName: IconName): string {
  return icons[iconName].label
}

console.log(iconLabel('arrowLeft'))
// "Left Arrow"

/* console.log(iconLabel('bob'))

   ERROR: Argument of type '"bob"' is not assignable to parameter of type
   '"arrowDown" | "arrowLeft"'. */
```

The key (no pun intended) line is this one: `type IconName = keyof typeof icons`. TypeScript is examining the `icons` object during typechecking, making a list of all its keys, and turning that into a union of the keys as strings:

```
arrowDown, arrowLeft => 'arrowDown' | 'arrowLeft'
```

TypeScript is able to do this because it enforces that all the object keys must be known during typechecking. It doesn't allow you to, for example, assign a new key like `icons.bob = 'Bob Arrow'`, because once it sees the object literal's keys, it decides those are the only ones that are allowed.

This is great for all the reasons that Bernhardt goes into detail in his post, but long story short it's a convenient way to automatically tie together the icon data to the icon label getter function and make sure they never go out of sync with each other.

When I saw this I wondered if a straight translation to Reason would be possible, because Reason doesn't have TypeScript's level of support for union types. It does, however, have [polymorphic variant types](http://reasonmlhub.com/exploring-reasonml/ch_polymorphic-variants.html) which have much the same level of power.

## The Reason version

After some trial and error, I came up with this:

```ocaml
let icons =
  fun
  | `arrowDown => {"label": "Down Arrow"}
  | `arrowLeft => {"label": "Left Arrow"};

let iconLabel = name => icons(name)##label;

let () = Js.log(iconLabel(`arrowLeft));
// "Left Arrow"

/* let () = Js.log(iconLabel(`bob));

   ERROR:

   This has type:
     [> `bob ]
   But somewhere wanted:
     [< `arrowDown | `arrowLeft ]
   The second variant type does not allow tag(s) `bob */
```

The biggest difference in the Reason version is that `icons` is not an object, but a function. This is because of the way polymorphic variant inference works. Only by using a function can we guarantee that the function will accept _only_ valid values of the variant. In this way we emulate a union type.

A slightly smaller but still convenient difference: we don't need to annotate the `iconLabel` function type. Reason automatically infers from usage that its parameter must be the polymorphic variant type that the `icons` function takes as well, and that its return type must be a `string` because that's the type of an icon object's `label` prop. This is all completely type-safe for the same reasons the TypeScript version is, and offers the same maintenance and refactoring benefits.