Introduction

I recently discovered the Rescript programming language, and have become very excited about it. There aren’t a lot of tutorials on it yet. I collect some links below, but I decided it was worth writing my own.

This is the first in a long-multipart series on Rescript. Will update with links to other parts when I write them.

This part is about migrating the create-react-app boilerplate to typescript later articles, I’ll turn it into a complete app.

Patreon

This series takes a lot of time to write and maintain. I’ve started it while I have some free time between jobs, but I’ll probably need some extra motivation to keep working on it once I’m busy with work again.

I’m not one for begging for money on the Internet, but if you’re interested in providing some of that motivation, I’ve set up a Patreon account. I thank you for your support.

As a bonus for patrons, articles will be published there at least two weeks before they make it to my public blog.

Table of Contents

Introducing Rescript

Rescript is a functional language derived from Reason. Reason itself is derived from a merging of OCAML and Javascript. Rescript is not actualy a new language; it’s a rebranding of Bucklescript. Bucklscript was originally just a toolchain to compile OCAML/Reason to Javascript. But now it is its own language. You’ll still see references to bucklescript in the ecosystem, as well as the bs prefix and files that end in .bs.js.

Rescript has a very comfortable Javascript-like syntax and outstanding JS interop. I really like the syntax and design decisions I’ve seen. Coding in Rescript, compared to Typscript feels as much safer than Typescript does compared to Javascript.

I have a feeling this will be the Next Big Thing. It feels that much safer than typescript and the compiler is much more helpful. It is also blazing fast. You will not believe how much quality of life difference it makes to have things compile this fast!

I also like how little config is required, though it is more than it should be. I have wasted way too much time trying to set up linting in both JS and TS! Like Typescript, the output is raw JS, so you can use your usual JS tooling.

Prior knowledge

I am assuming you already know React and its underlying technologies. I make references and comparisons to Typescript, but you don’t need to know it. You should have passing knowledge of functional programming principles. If you know redux and an immutable library, that’s sufficient. You should have some knowledge of static type analysis. This could be gradual typing such as Typescript, Flow, Python type hints, or static typing such as Rust, Go, Java, or C No need to know Reason or OCAML (I don’t!)

Technologies

I’m keeping this light, so just a couple libraries:

Let’s begin

First, install the Rescript compiler:

  • npm install -g bs-platform
  • Run bsb -version to make sure it’s installed

For what it’s worth, my version is 9.0.0 at time of writing. You’ll probably want to install an editor language extension for rescript:

I should note that editing Rescript is a dream because the lint errors show up instantaneously. The compiler is that fast. It’s amazing. I actually had to install a faster terminal emulator so neovim could keep up!

A couple wild geese

To save you wasting some time, here are a couple dead ends I went down:

First, don’t use ReasonReact. Use rescript-react. It’s more or less just a renaming of ReasonReact, but it’s been and will continue to be, updated to work with the newer Rescript ecosystem.

Second, there is a rescript template for create-react-app You can install it with npx create-react-app --template=rescript rescript-react-intro It does not seem to be a very popular repository, yet and hasn’t been updated in four months. I compared the template to the default create-react-app template at time of writing, they are virtually identical .

Honestly, the template would probably work for you, but for my edification (and yours), I decided to start with a default create-react-app install. I’m keeping this repo open beside me for reference, and it’s close to what ’ll end up with. In fact, you could clone the branch linked at the end of this article to have a good starting point for your own Rescript-react project.

Begin (for real this time)

Just create a vanilla react app with the default Javascript template

npx create-react-app rescript-react-intro
cd !$

Now add a few dependencies:

npm install --save-dev bs-platform gentype
npm install --save @rescript/react # --force
  • bs-platform needs to be installed locally as well as globally
  • gentype probably isn’t needed for this project. It’s used for Typscript/Flow interop but it’s kind of standard to have it installed.
  • rescript-react is a very recently released port of the older ReasonReact project. I found that it didn’t want to install with the 9.0.0 version of Rescript, so I --forced it. This will probably bite me in the end, but I imagine by the time you read this, rescript-react will have released a fix.

Configure Rescript

Rescript needs a bit more configuration before we can use it. This is a sore point for me. I’ve been scarred by too many babel/webpack/eslint/tsconfig nightmares! It should not take half an hour of reading documentation to get config set up for hello world!

I think the Rescript team is working on improving the tooling. There’s quite a bit of legacy stuff going on from the Reason/bucklescript days, and one of the most exciting things about the language is how new it is and how quickly it is evolving.

For what it’s worth, I based my config on:

  • The one spit out by bsb -init react-hooks
  • The one spit out by bsb -init reason-react
  • The one spit out by create-react-app --template=rescript
  • The output of bsc -help
  • This tutorial
  • The official docs

You’ll need a new bsconfig.json file in the main directory, beside package.json:

{
  "$schema": "https://raw.githubusercontent.com/rescript-lang/rescript-compiler/master/docs/docson/build-schema.json",
  "name": "rescript-react-intro",
  "reason": {
    "react-jsx": 3
  },
  "sources": {
    "dir": "src",
    "subdirs": true
  },
  "bsc-flags": ["-bs-super-errors"],
  "refmt": 3,

  "package-specs": [
    {
      "module": "es6",
      "in-source": true
    }
  ],
  "suffix": ".bs.js",
  "namespace": true,
  "gentypeconfig": {
    "language": "typescript"
  },

  "bs-dependencies": ["@rescript/react"]
}

This whole file is basically an itemized list of complaints. Note: I love Rescript and I think they will fix this experience soon. This is the only time I’ll complain about the language in the whole series.

$schema is needed to keep my editor from complaining. The plugin is looking for a non-existent schema by default. This will probably be fixed in Rescript 9.0. - probably hasn’t been updated since bucklescript got renamed to rescript

The two 3s are suspect. They indicate the version of jsx and the rescript formatter to use. They don’t default to 3 for backwards compatibility, but they are the preferred setting.

The -bs-super-errors flag sounds great in the docs: “Better error message combined with other tools” so I don’t know why it isn’t on by default.

The .bs.js suffix is “recommended” in the docs (to help the compiler drop unused files), but the compiler outputs js by default. Recommendations should be defaults!

It’s unclear what all the different subconfiguration categories mean What’s the difference between a flag, a package-spec, and a source?

And last, but not least, what the hell is all this bs? Rescript is a recent rebranding, but hopefully this will change soon, too.

Anyway, at the time of writing, the above config should serve you fine. But I have fears that when I start my next rescript project I’ll have to do all this research all over again. It’s not the kind of thing you do often enough to memorize.

For convenience, make a quick addition to your package.json to make it easier to build and run rescript projects.

"start:res": "bsb -make-world -w",

You can now run this in one terminal and npm start in the other. Rescript will build your files so fast that they’ll be ready to go by the time the create-react-app script is ready to go.

Note: I actually don’t run this command. My editor builds the files for me so it can provide linting. So I don’t need to run a second builder.

Port create-react-app’s JS to rescript the easy way

Rescript might be building, but it doesn’t have anything to build yet!

There are five .js files in the src directory:

  • App.js
  • App.test.js
  • index.js
  • reportWebvitals.js
  • setupTests.js

Let’s start by pseudo-porting two of them. Rename the App.js and index.js files to have the .res extension:

git mv src/index.js src/Index.res
git mv src/App.js src/App.res

Then open both files and surround their contents with %%raw(...) For example, Index.res will now look like this, but do the same with App.res:

  %%raw(`
  import React from 'react';
  import ReactDOM from 'react-dom';
  import './index.css';
  import App from './App';
  import reportWebVitals from './reportWebVitals';

  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    document.getElementById('root')
  );

  // If you want to start measuring performance in your app, pass a function
  // to log results (for example: reportWebVitals(console.log))
  // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
  reportWebVitals();
  `)

If you run npm start now, you’ll get an error message: “Could not find required file index.js”

This is because webpack is configured by default to access index.js - But the output of compiling Index.res is Index.bs.js. I configured Rescript to put it in the same directory as the other sources, so it won’t be too hard to find. But rather than messing with the configuration, just add a new index.js file with one line:

import "./Index.bs";

This is the approach recommended by rescript. Check in your JS sources so reviewers can see how the compiled output changes. Rescript compiles to very readable Javascript (it’s a design goal). In fact, take a look at the Index.bs.js and App.bs.js files in the src folder.

For now, it’s just the raw JS from the .res files. Because we’re importing Index.res from index.js, create-react-app is just looking at Javascript. It doesn’t even know rescript exists.

npm start:res should now compile successfully, but if you load it in the browser there will be an import error. Let’s fix it.

Edit Index.res to change the line import App from './App' to import App from './App.bs'. As soon as you save, the file will compile to ./Index.bs.js In milliseconds!

  • Refresh the browser window and you should see the default react app again

Porting App.res (for real this time)

The two .res files are technically valid rescript: %%raw is a legitimate rescript command. But, of course, it loses all the purported type safety of rescript. It’s a perfectly reasonable choice if you’re in a hurry, but we can do better. Rescript’s recommended porting strategy is to leave some stuff in %%raw and migrate the stuff that has an obvious rescript implementation first. Check the JS output and repeat.

Leave the two import statements in %%raw since I’m Not sure how to import CSS or svg from rescript yet. But it’s easy to migrate a function definition.

Change function App() {} to let make = () => {}.

make is a Rescript convention for what you might call a “constructor” in rescript. The React-rescript macros require that we name it thus. We use “fat arrow” syntax because that is how a function is defined in rescript.

Also add a @react.component decorator before the function definition. This decorator instructs react-reason to reformat the function a bit. Finally, remove the return keyword. In rescript, the last line of the function is automatically returned.

The jsx itself can stay pretty much identical except for a couple minor changes:

First, you need to explicitly tell it to insert React strings for text. This makes it a bit more verbose, but it guarantees sound compile-time type safety. You have to (get to) specify exactly what the compiler should expect. So you’ll never pass an array of elements where a string is expected or, worse, undefined.

So, for example, change Edit <code>src/App.js</code> and save to reload. to:

{React.string("Edit ")}
<code>{React.string("src/App.res")}</code>
{React.string(" and save to reload.")}

Similarly, change Learn React to {React.string("Learn React")}

You’ll also need to change <img src={logo}... to <img src={%raw("logo")} since Rescript doesn`t know about the existence of the “logo” (the import itself was defined in raw JS).

Lastly, remove export default App. Rescript doesn’t have the concept of exports. By default, every type in a file is pubilcly accessible, although there are ways to enforce an interface.

This is how the file looks now:

%%raw(`
import logo from './logo.svg';
import './App.css';
`)

@react.component
let make = () => {
  <div className="App">
    <header className="App-header">
      <img src={%raw("logo")} className="App-logo" alt="logo" />
      <p>
        {React.string("Edit ")}
        <code> {React.string("src/App.js")} </code>
        {React.string(" and save to reload.")}
      </p>
      <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">
        {React.string("Learn React")}
      </a>
    </header>
  </div>
}

This isn’t running yet due to an import error in Index.res, but it compiles correctly.

Pedantically removing raw JS

To be pedantic, I want to remove the remaining %raw calls. However, the CSS one seems to be standard so I’ll leave it. I have a later article queued up to tackle styling. Importing the svg can be done using:

@module("./logo.svg") external logo: string = "default"

Don’t forget to remove the import from inside %%raw.

Finally, revert the %raw("logo") change we just made to logo in the <img src../>

Here is the final file:

%%raw(`import './App.css';`)

@module("./logo.svg") external logo: string = "default"

@react.component
let make = () => {

  <div className="App">
    <header className="App-header">
      <img src={logo} className="App-logo" alt="logo" />
      <p>
        {React.string("Edit ")}
        <code> {React.string("src/App.js")} </code>
        {React.string(" and save to reload.")}
      </p>
      <a className="App-link" href="https://reactjs.org" target="_blank" rel="noopener noreferrer">
        {React.string("Learn React")}
      </a>
    </header>
  </div>
}

It’s educational to take a look at the output App.bs.js file. It’s shockingly readable, though not as similar to hand-written JS as I expected based on the hype. The worst part is that the JSX has been translated to their relevant function calls. This is necessary, of course, since JSX itself is not valid Javascript!

The most notable difference is that it is exporting make as a named export instead of a default - This is breaking Index.res which we’ll port next

Fixing a couple bugs in Index.res

I did App.res first because rescript recommends porting leaf JS files before their parents. In other words, files that don’t import any JS code that you wrote should be ported before files that call them.

If you run npm start now, you will get an import error:

'./App.bs does not contain a default export'

This is easily fixed in the %raw JS in Index.res. Just change import App from './App.bs' to 'import {make as App} from './App.bs.

If you run start:cra now, everything should load and look like the default app. But we aren’t done! There is still a big block of %%raw in Index.res.

Before we address that, though, let’s fix a subtle bug that exists in virtually every react application.

document.getElementById('root') might return undefined if that element doesn’t exist. To illustrate, go ahead and change 'root' to 'doesnotexist'. Your react app browser window will show an error.

Good news, though: this is a bug that cannot happen in rescript! But before we go there, let’s fix it in the %raw js code:

const root = document.getElementById("doesnotexist");

if (root) {
  ReactDOM.render(
    <React.StrictMode>
      <App />
    </React.StrictMode>,
    root
  );
}

Now a blank page will load because there is no doesnotexist element. That’s much better than an error!

Porting Index.js to proper Rescript code

Now let’s start teasing out some of that raw Javascript. The reportWebVitals import seems simple and obvious, so let’s change it first. Split the %%raw block in two:

  • one for the imports
  • one for the rest of the block

Then remove the reportWebVitals import and add a @module("./reportWebVitals") external reportWebVitals: 'whatever = "default" between the two %%raw blocks.

These interop exports to JS are long and a bit confusing so let’s break it down:

  • @module("./reportWebVitals") external is the syntax for binding a Rescript name to a javascript value
  • reportWebVitals: is the name that it is being bound to
  • 'whatever is the type it is getting bound to. In this case, we aren’t bothering to tell rescript anything about the type. 'whatever is a generic type. This obviously isn’t very type safe so we will revisit it later = "default" is the “name of the js value you are referring to”, in the words of the docs. In this case, the default export of the module but it could be any exported value in the module.

If you save the file now, it will compile and load correctly. If it doesn’t, you may have forgotten to change the doesnotexist back to root first.

Also move the reportWebVitals(); line outside of the %%raw block. You can lose the semicolon since Rescript doesn’t need them. The function call syntax is the same for JS and Rescript, so the line can be otherwise unmodified.

Now let’s replace the rest of the lines in %%raw code blocks. Start by removing all of the raw imports except import './index.css' Rescript is able to automatically import ReactDOM and App based on the filenames.

Then replace the const root = declaration with let rootQuery = ReactDOM.querySelector("#root")

In Rescript, let is immutable, so it does the same thing as const in JS. The truth is, you rarely want to mutate values in functional languages. This is great because “Immutable by default” maps very well to redux-style development. I love the boilerplate you’ll see in the actions and reducers part of this series (spoiler alert: there is no boilerplate).

ReactDOM is now using the react-reason version of ReactDOM instead of the JS import. It is available in your namespace because you put reason-react in both your bsconfig.jsonand package.json.

querySelector is a ReactDOM function. I named the result rootQuery to indicate that it returns an Option, not an element.

As with all jquery-style selector mini-languages, the # in the selector indicates an ID selection.

Now let’s see some Rescript magic. Replace the entire if statement with:

switch rootQuery {
| None => ()
| Some(root) => ReactDOM.render(<App />, root)
}

Unlike in Javascript, switch demands that you cover all options in the pattern. In this case, because an Option was returned, the two possible values are None and Some(element)

As an exercise, try removing the None line and check the warning output. The | symbol indicates one possible pattern in the switch. I consistently forget this symbol, probably because the similar construct in Rust does it slightly differently and that’s what I’m more familiar with.

Some(root) is simultaneously destructuring the option and binding a value to root. We can then pass that value, which is now guaranteed to exist, into the ReactDOM.render method.

As one last change, let’s narrow the scope on the 'whatever type for reportWebVitals. reportWebVitals is a function that accepts no arguments and returns nothing. In Rescript, “Nothing” is represented in Rescript as unit.

Note that unitis not the same as null/undefined. It is not a value you can assign or compare to; it is just a marker indicating that there is nothing being returned, or that a function accepts no arguments.

Since this function has neither arguments nor return value, the type signature is unit => unit. So change the rootWebVitals external line to @module("./reportWebVitals") external reportWebVitals: unit => unit = "default"

This is more exact than 'whatever, and is safer. Anywhere in your rescript code that calls this function will be checked to ensure it matches this type. However, it is still a potetial source of errors; it is possible the upstream This is inevitable when working with Javascript. JS simply isn’t as safe as Rescript. But all your new Rescript code has type safety guarantees. So write lots of Rescript.

To wrap it up, here’s the final Index.res file:

%%raw(`import './index.css';`)
`@module("./reportWebVitals") external reportWebVitals: unit => unit = "default"`

let rootQuery = ReactDOM.querySelector("#reactroot")

switch rootQuery {
| None => ()
| Some(root) => ReactDOM.render(<App />, root)
}

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals()

If, in the future, you are looking for a boilerplate to start your CRA Rescript project from you can this branch, which contains the state of this article.

Conclusion

So far in my explorations, I love this language. It is much more concise than Javascript and it compiles so much faster than Typescript. I love how it allows my editor to lint and reformats instantly. I never noticed how irritating that delay was until I no longer had to deal with it!

Notice how much more concise Rescript is and yet, it’s also soundly type-checked!

This is just the beginning. Please see my other articles, and there are many more to come!