Calling ReasonML from TypeScript (or Flow), the easy way
Integrating ReasonML to an existing TypeScript/Flow codebase may sound difficult... but it's actually easier than you think!

#A bit of context

I've recently started to work on a component library that implements some design system specifications. It will make it easier to create more complex user interfaces and website features maintaining consistency between pages look & feel, user experience and brand style in general on a product that I'm working on.

We've opted for building it using React, styled-components and TypeScript so that every component will be easier to abstract, develop and test. We also want to handle multiple UI themes (dark/light) and why not, maybe support other themes in the future, and styled-components has an awesome built-in theme provider that does that exact job.

So, let's see how a simple button would be implemented using these technologies:

import * as React from "react";
import styled from "styled-components";
import { theme } from "./types.d";

type ButtonProps = {
  color?:    "primary" | "red" | "yellow" | "white";
  size?:     "small" | "medium" | "large";
  outlined?: boolean;
  theme:     theme;
};

const Button = styled.button`
  color:      ${(theme): string => theme.text.primary};
  background: ${(theme): string => theme.background.primary};
  ${/* other styles */}
`;

of course this is just a simplification over the actual button component, but it allow us to understand how we started to build our components.

So, depending on the current theme (light/dark), our button would look like this:

Great. Now the problem was that we needed multiple styles (outlined button, with a custom icon, different colors and so on), so we wanted to build a very abstract component:

And we ended up with something like this:

import * as React from "react";
import styled from "styled-components";
import { theme } from "./types.d";
import { getColor, getSize, getOutline } from "./helpers";

type ButtonProps = {
  color?:    "primary" | "red" | "yellow" | "white";
  size?:     "small" | "medium" | "large";
  outlined?: boolean;
  theme:     theme;
};

const Button = styled.button`
  ${getColor}
  ${getSize}
  ${getOutline}
  ${/* other styles */}
`;

where every helper looked like this:

import { theme } from "./types.d";

type ButtonProps = {
  color?:    "primary" | "red" | "yellow" | "white";
  size?:     "small" | "medium" | "large";
  outlined?: boolean;
  theme:     theme;
};

type ColorTuple = [string, string];

const useColors = ({ color, theme }: ButtonProps): ColorTuple => {
  switch (color) {
    case "primary":
      return [theme.text.primary, theme.colors.primary];
    case "white":
      return [theme.colors.dark, theme.colors.white];
    case "red":
      return [theme.colors.white, theme.colors.red];
    case "yellow":
      return [theme.colors.dark, theme.colors.yellow];
    default:
      throw Error(`Unknown color: ${color}`);
  }
}

const getColor = ({ color, theme }: ButtonProps) => {
  const [ color, background ] = useColors({ color, theme });

  return `
    color:      ${color};
    background: ${background};
  `;
}

and so on with all the other helpers (once again, I'm just semplifying things in the above example). But as you can see, here we're actually pattern-matching against a specific color, then we're extracting a "tuple" of values that can be described as follows: [TextColor, BackgroundColor].

Doing that in TypeScript is pretty easy, as you can see, but we're implementing a pattern which is widely used in other programming languages such as Elixir, Haskell... and ReasonML! So, what if I have to pattern-match against a touple? What if I have to make more complex computations in order to generate the correct string? For a FP-junkie like me that chould be a great alternative to TypeScript, so why not?

So here came the idea of refactoring at least those helpers using ReasonML, maintaining the type interoperability with TypeScript.

#How ReasonML makes it easier

First of all, we need to define our ReasonML types. Let's start with the ButtonProps types (we'll leave out theme for now):

type color =
  | Primary
  | Red
  | Yellow
  | White;

type size =
  | Small
  | Medium
  | Large;

type outlined = bool;

type buttonProps = {
  color,
  size,
  outlined
};

if we try to implement the getColor helper, we would end up with writing something like this:

let useColors = (color, theme) =>
  switch (color) {
    | Primary => (theme.text.primary, theme.colors.primary)
    | Red     => (theme.colors.white, theme.colors.red)
    | Yellow  => (theme.colors.dark,  theme.colors.yellow)
    | White   => (theme.colors.dark,  theme.colors.white)
    | _       => ("", "")
  };

let getColor = (color, theme) => {
  let (color, background) = useColors(color, theme);
  {j|
    color:      $color;
    background: $background;
  |j}
};

it feels incredibly natural to write that kind of functions in ReasonML! Pattern matching is a widely used feature in functional programming languages, but there's not a stable specification yet for implementing it in the next EcmaScript versions (there's just a proposal and a Babel plugin, learn more here).

#Interoperability between ReasonML and TypeScript

We've just scratched the surface of the reasons you should try Reason! (I'm not funny, I know) But now that we wrote our ReasonML function, how do we call it from TypeScript? Believe it or not, it is way easier than you think!

First of all, let's write down the types for our theme:

type textTheme = {
  primary: string,
  dark: string,
  white: string,
};

type themeColors = {
  primary: string,
  dark: string,
  white: string,
  yellow: string,
  red: string,
};

type theme = {
  text: textTheme,
  colors: themeColors,
};

Now let's rewrite our helper functions with type annotations, just to make it easier to read:

let useColors = (inputColor: color, inputTheme: theme): (string, string) =>
  switch (inputColor) {
  | Primary => (inputTheme.text.primary, inputTheme.colors.primary)
  | Red     => (inputTheme.colors.white, inputTheme.colors.red)
  | Yellow  => (inputTheme.colors.dark,  inputTheme.colors.yellow)
  | White   => (inputTheme.colors.dark,  inputTheme.colors.white)
  | _       => ("", "")
  };

let getColor = (color: color, theme: theme): string => {
  let (color, background) = useColors(color, theme);
  {j|
    color:      $color;
    background: $background;
  |j};
};

Awesome! Now let's add two new dependencies to our typescript codebase:

yarn add -D bs-platform gentype

the first one is required for compiling ReasonML to JavaScript via the Bucklescript compiler, the second one is used for generating TypeScript (or even Flow) compatible types.

Let's add two new scripts to the package.json file:

{
...
  "start:re": "bsb -make-world && react-scripts start",
  "build:re": "bsb -make-world && react-scripts build"
...
}

great, we just need to create a new file, called bsconfig.json (which is similar to the Node.js' package.json file):

{
  "name": "my-awesome-ts-re-project",
  "sources": [
    {
      "dir": "src",
      "subdirs": true
    }
  ],
  "package-specs": [
    {
      "module": "es6-global",
      "in-source": true
    }
  ],
  "suffix": ".bs.js",
  "namespace": true,
  "refmt": 3,
  "gentypeconfig": {
    "language": "typescript"
  }
}

As you can see, you can specify which kind of types you should generate. In that case, we'll generate TypeScript-compatible types. There's just one thing missing; we need to specify to the BuckleScript compiler which functions should be analyzed for types generation, and that's incredibly easy to do:

[@genType]
let useColors = (inputColor, inputTheme) =>
  switch (inputColor) {
  | Primary => (inputTheme.text.primary, inputTheme.colors.primary)
  | Red     => (inputTheme.colors.white, inputTheme.colors.red)
  | Yellow  => (inputTheme.colors.dark,  inputTheme.colors.yellow)
  | White   => (inputTheme.colors.dark,  inputTheme.colors.white)
  | _       => ("", "")
  };

[@genType]
let getColor = (color: color, theme: theme): string => {
  let (color, background) = useColors(color, theme);
  {j|
    color:      $color;
    background: $background;
  |j};
};

we can do so by just adding [@genType] before function declaration!

Now we're ready for building our ReasonML functions by running yarn build:re. It will generate two new files in the same directory of our original .re file (let's say we called it helper.re): helper.bs.js and helper.gen.tsx. Let's see how do they look:

helper.bs.js

// Generated by BUCKLESCRIPT, PLEASE EDIT WITH CARE


function useColors(inputColor, inputTheme) {
  switch (inputColor) {
    case /* Primary */0 :
        return /* tuple */[
                inputTheme.text.primary,
                inputTheme.colors.primary
              ];
    case /* Red */1 :
        return /* tuple */[
                inputTheme.colors.white,
                inputTheme.colors.red
              ];
    case /* Yellow */2 :
        return /* tuple */[
                inputTheme.colors.dark,
                inputTheme.colors.yellow
              ];
    case /* White */3 :
        return /* tuple */[
                inputTheme.colors.dark,
                inputTheme.colors.white
              ];

  }
}

function getColor(color, theme) {
  var match = useColors(color, theme);
  return "\n    color:      " + (String(match[0]) + (";\n    background: " + (String(match[1]) + ";\n  ")));
}

export {
  useColors ,
  getColor ,

}
/* No side effect */

As you can see, the BuckleScript-generated file looks incredibly similar to the original TypeScript solution we wrote in the first paragraph!

helper.gen.tsx:

/* TypeScript file generated from helpers.re by genType. */
/* eslint-disable import/first */


const $$toRE841136: { [key: string]: any } = {"Primary": 0, "Red": 1, "Yellow": 2, "White": 3};

// tslint:disable-next-line:no-var-requires
const Curry = require('bs-platform/lib/es6/curry.js');

// tslint:disable-next-line:no-var-requires
const helpersBS = require('./helpers.bs');

// tslint:disable-next-line:interface-over-type-literal
export type color = "Primary" | "Red" | "Yellow" | "White";

// tslint:disable-next-line:interface-over-type-literal
export type textTheme = {
  readonly primary: string; 
  readonly dark: string; 
  readonly white: string
};

// tslint:disable-next-line:interface-over-type-literal
export type themeColors = {
  readonly primary: string; 
  readonly dark: string; 
  readonly white: string; 
  readonly yellow: string; 
  readonly red: string
};

// tslint:disable-next-line:interface-over-type-literal
export type theme = { readonly text: textTheme; readonly colors: themeColors };

export const useColors: (inputColor:color, inputTheme:theme) => [string, string] = function (Arg1: any, Arg2: any) {
  const result = Curry._2(helpersBS.useColors, $$toRE841136[Arg1], Arg2);
  return result
};

export const getColor: (color:color, theme:theme) => string = function (Arg1: any, Arg2: any) {
  const result = Curry._2(helpersBS.getColor, $$toRE841136[Arg1], Arg2);
  return result
};

this is a bit more complicated, and takes advantage of the bs-platform library in order to work. But as you can see, it's exposing types exactly in the same way we coded it in the first paragraph! It also automatically adds Eslint:disable comments so that it won't conflict with your Eslint configuration.

How awesome is that?

What will you build with ReasonML and TypeScript? I bet you'll do something great!