DEV Community

Yawar Amin
Yawar Amin

Posted on

Emulating TypeScript union types with ReasonML, part 2

IN a previous post, I showed a technique in Reason for automatically creating a 'union type' like the ones that TypeScript has. In essence, this TypeScript union type:

type Labels = 'Down Arrow' | 'Left Arrow'
Enter fullscreen mode Exit fullscreen mode

Can be modelled with a Reason function:

let labels = fun
  | `DownArrow => "Down Arrow"
  | `LeftArrow => "Left Arrow";
Enter fullscreen mode Exit fullscreen mode

...and Reason infers that the function has type:

let labels: [< `DownArrow | `LeftArrow] => string;
Enter fullscreen mode Exit fullscreen mode

Of course, this is not exactly the same–it's more of a way to convert a limited set of polymorphic variant values into a limited set of strings. We can think of it as a 'poor man's union type'.

Slightly richer

But Reason's abilities don't stop there. The BuckleScript compiler actually has a feature, called @bs.deriving jsConverter, that can do a two-way conversion between a polymorphic variant type and string values. Here's the previous example, converted to use it:

[@bs.deriving jsConverter]
type label = [
| [@bs.as "Down Arrow"] `DownArrow
| [@bs.as "Left Arrow"] `LeftArrow
];
Enter fullscreen mode Exit fullscreen mode

If you look at the JavaScript output for this, you'll notice the following functions: labelToJs, labelFromJs. These convert the polymorphic variant values to strings and vice-versa. The cool part is that the labelToJs conversion is exactly what we hand-wrote above, so if you pass in an unsupported value you get a type error:

/* let test = labelToJs(`DownArray);

We've found a bug for you!
OCaml preview 5:22-32

This has type:
  [> `DownArray ]
But somewhere wanted:
  label
The second variant type does not allow tag(s) `DownArray */
Enter fullscreen mode Exit fullscreen mode

We can think of this as not exactly a union type, but a type that is 'isomorphic' to a union type. Pretending for a second that Reason had real union types:

let labelToJs: [`DownArrow | `LeftArrow] => "Down Arrow" | "Left Arrow"
Enter fullscreen mode Exit fullscreen mode

Reason does a lot of things by spreading the workload (so to speak) between types and functions. Data types describe the data and function types describe the relationships between them. And even though Reason doesn't have real union types, we can still take advantage of guarantees from features like @bs.deriving jsConverter to come close.

Top comments (2)

Collapse
 
hoichi profile image
Sergey Samokhov

Funny how in this case TypeScript’s abstraction is zero-cost where Reason’s is not. No wonder, of course, since TS more closely matches the JS semantics.

Then again, a string representation of variant types is probably ever needed for interop only.

Collapse
 
yawaramin profile image
Yawar Amin

True, and for those we have other techniques.