Rescript from a JS dev point of view

14 min read

You all know that I'm fascinated to learn new languages, I like to study compilers/transpilers, etc.. the last two languages I've been learning are Go (thanks to Guilherme that influenced me and convince me to use in a side project that we are doing together 💜, this is one of the best engineers I know) and Rescript. You also know, that I make money as a JS/front-end engineer and I love it, even though with all the problems we have with JS, still an amazing language, flexible enough for a beginner to start and for a really experienced programmer to use every day and enjoy it, as well it has the flexibility to run everywhere! Web, Mobile, Desktop, hardware, you choose! The community is amazing and the ecosystem is soo active, at this point we may have a few hundred new npm libs available.

This is a presentation and a blog post at the same time, just explaining a few points as a JS dev learning Rescript that I considered to say: "you know what? it worth it! Worth learning Rescript language and use it". This is not a post to say I hate TS, just because, I don't. I've been using TS for the last two years and enjoying it, yet, I can see problems and room to improve as any other language in the world.

In the end, all I want is to feel productive using something to solve problems, but the right problems. I want to feel confident that I can work in a big refactor and if the compiler compiled with success everything is right, for real. I want to write less and do more because I know the compiler won't let someone use my functions passing the wrong values, a better inference. I want it to be FAST, so fast that I will save the file again just to make sure it is right instead of opening a new Twitter tab, Rescript.

That was the biggest intro I've ever done, which may show how excited I am. Below are the points I considered important to learn Rescript and why I would use it in a day-to-day project. It doesn't mean I'm right, just opinions.

JS Interop

Here's something for you: JS is the web language! JS is everywhere! and always bet on JS! even if you don't like it, it is true. So one of the first points that I took a look at was how easy or difficult it would be to use any js lib/ js code without the need to rewrite it to Rescript.

and why? because I don't want to stop using JS. If something super cool like Xstate appears or a new browser API, I'd like to still be able to use it on my Rescript code anyway, even if the language doesn't have official support for it. It needs to be easy to maintain and fast to create if needed.

Let's say I'd like to use Lodash, waiting for comments: You don't need to use Lodash yada yada yada.. bleee, I bet it saved your ass MANY times and it is just an example, anyway, everything you would need to do would be simply defining some types like you would do with TS if you want to have some type safety in your code. e.g:

We could create a Lodash.res definitions file:

@module("lodash/chunk")
external chunk: (array<'a>, int) => array<array<'a>> = "default"

and then in another file whenever you want to use it, it would be just a matter of doing it:

let myArray = [1,2,3]
let chunks = Lodash.chunk(myArray, 2)

you could also export many functions, backing to Lodash.res again:

@module("lodash")
external chunk: (array<'a>, int) => array<array<'a>> = "chunk"

@module("lodash")
external difference: (array<'a>, array<'a>) => array<'a> = "difference"

and use in the same way:

let myArray = [1,2,3]
let chunks = Lodash.chunk(myArray, 2)
let difference = Lodash.difference(myArray, [2])

Readable output

The output code that Rescript produces is human readable, clean, and minimal which makes the bundle size to be the same as some human coding JS. Here is the output from the example above:

// Generated by ReScript, PLEASE EDIT WITH CARE

import * as Lodash from "lodash";

var myArray = [
  1,
  2,
  3
];

var chunks = Lodash.chunk(myArray, 2);

var difference = Lodash.difference(myArray, [2]);

export {
  myArray ,
  chunks ,
  difference ,
  
}
/* chunks Not a pure module */

Is it Rescript or JS?

Another important point is that the syntax is quite similar to JS, Rescript is another language different than Typescript that is built on top of JS. So having a similar syntax helps a lot to get on track Fast! To prove this point let's do an exercise called "is it Rescript or JS?"

is it Rescript or JS?

let person = {
  "age": 5,
  "name": "Big ReScript"
}

Rescript!

is it Rescript or JS?

let add = (a, b) => a + b
let addTwo = add(2)
let test = addTwo(10) // 12

Rescript! JS is not curried by default, more about that below.

is it Rescript or JS?

let myArray = ["hello", "world", "how are you"]

let firstItem = myArray[0] // "hello"

Rescript!

In fact, almost all the examples would work in JS and Rescript world. The difference is that with Rescript we would have a compiler with an amazing type inference system that would help us write better/safe code and we even didn't need to write any type yet.

Of course, we do have a few differences in syntax and features, but they are so minimal that is just a matter of knowing that they exist, for example, if statements:

let showMenu = true;

if showMenu {
  displayMenu()
} else {
  Js.log("nothing here...")
}

we just don't need to use parentheses.

No imports

Unlike JS, Rescript doesn't have export or import statements, what happens in Rescript is that every file is a module and its name needs to be unique. Even files within folders are accessible on the same level as anything else. You just need to use the FileName which needs to be in CamelCase and then dot type/method name and use it.

you can also use open and instead of using ModuleName.Something all the time you would do:

open Lodash

let myArray = [1,2,3]
let chunks = chunk(myArray, 2)

Having no imports/exports and module by file system also forces us to keep a more flat structure that has some big advantages long term.

From the rescript documentation:

By default, every file's type declaration, binding and module is exported, aka publicly usable by another file. This also means those values, once compiled into JS, are immediately usable by your JS code.

Type system

Well, I could write something with my own words, but the Rescript team did an amazing job talking about Types on the Rescript docs page, so, from Rescript docs:

Types are the highlight of ReScript! They are:

Strong. A type can't change into another type. In JavaScript, your variable's type might change when the code runs (aka at runtime). E.g. a number variable might change into a string sometimes. This is an anti-feature; it makes the code much harder to understand when reading or debugging.

Static. ReScript types are erased after compilation and don't exist at runtime. Never worry about your types dragging down performance. You don't need type info during runtime; we report all the information (especially all the type errors) during compile time. Catch the bugs earlier!

Sound. This is our biggest differentiator versus many other typed languages that compile to JavaScript. Our type system is guaranteed to never be wrong. Most type systems make a guess at the type of a value and show you a type in your editor that's sometime incorrect. We don't do that. We believe that a type system that is sometime incorrect can end up being dangerous due to expectation mismatches.

Fast. Many developers underestimate how much of their project's build time goes into type checking. Our type checker is one of the fastest around.

Inferred. You don't have to write down the types! ReScript can deduce them from their values. Yes, it might seem magical that we can deduce all of your program's types, without incorrectness, without your manual annotation, and do so quickly. Welcome to ReScript =).

Let's explore a bit about inference, which is the most exciting part about Rescript language, write less and do more!

let's consider the following function:

let add = (a, b) => a + b

here's the compiler error if you try to use this function passing two strings instead of numbers:

We've found a bug for you!
  /Users/dielduarte/localhost/testing-rescript/src/ExternalLibs.res:3:5-8

  1let add = (a, b) => a + b
  2
  3add("11", "2")

  This has type: string
  Somewhere wanted: int

  You can convert string to int with Belt.Int.fromString.

FAILED: cannot make progress due to previous errors.
>>>> Finish compiling(exit: 1)

Wow, it looks like how TS compiler should be 😅 with Rescript I feel like I'm pair programming all the time. The compiler shows the error, why, and yet, how to solve it and we didn't even write any type.

But how is that possible? well, in Rescript using + is just valid for numbers, so the compiler infers by default that the function Add just works for numbers. If you want to concatenate strings you should use ++.

Variants

Most data structures in most languages are about "this and that". A variant allows us to express "this or that".

Variants at first, look like enums with superpowers.

a simple example would be:


type myResponse =
  | Yes
  | No
  | PrettyMuch

let areYouCrushingIt = Yes

hello enums, my old friend.

But then comes the superpowers, a variant that can contain constructors arguments separated by a comma. e.g:

type account =
  | None
  | Instagram(string)
  | Facebook(string, int)

so then for the same type variant, we could use:

let myAccount = Facebook("Josh", 26)
let friendAccount = Instagram("Jenny")

it can also receive a record (object):

type user =
  | Number(int)
  | Id({name: string, password: string})

let me = Id({name: "Joe", password: "123"})

me still from type user, but a different variant. Using pattern matching down the line and variants is such a powerful technique, and can even avoid a few performance issues like you can see here where we reduced our program complexity from 0(n) to 0(1).

Pattern matching

If variants are enums with superpowers, pattern matching is the switch with superpowers. Mixing both are 🤯

We can destruct any data using a switch to match patterns (pattern matching) in many different ways, the example below is matching the type used as any Number(id), or Id({ name: "Joe" }) an Id with the name equal Joe or any Id(options).


type user =
  | Number(int)
  | Id({name: string, password: string})

let me = Id({name: "Joe", password: "123"})

switch me {
| Number(id) => Js.log("Your id is => " ++ Js.Int.toString(id))
| Id({name: "Joe"}) => Js.log("Welcome Joe!")
| Id(option) => Js.log("Welcome =>" ++ option.name)
}

Pattern matching can be used to match any type, lists, arrays, tuples, variants, and more. And as it wasn't enough it is also exhaustive. This means that every time you are matching a type you should check for every different pattern that the type you are checking might be, and, if you forget about it, the compiler will remind you. Let's suppose in the example above I forgot to handle the Id variant, then the compiler would say:

Warning number 8
  /Users/dielduarte/localhost/test-rescript/src/ExternalLibs.res:7:1-9:1

  5let me = Id({name: "Joe", password: "123"})
  6
  7switch me {
  8| Number(id) => Js.log("Your id is => " ++ Js.Int.toString(id))
  9}

  You forgot to handle a possible case here, for example:
  Id _

>>>> Finish compiling 128 mseconds

Curried by default

This is one of the curiosities about Rescript language that I most enjoyed. All functions in Rescript are curried by default, which means that you can apply partial application whenever you feel that is required and write less code.

In javascript, to create that example we saw above we would need to use closures or use a helper like Lodash curry:

let add = (a) => (b) => a + b //closure
let addTwo = add(2)
let test = addTwo(10) // 12

In Rescript we would need to just create a regular function and use partially:

let add = (a, b) => a + b
let addTwo = add(2)
let test = addTwo(10) // 12

Labeled arguments

In Javascript/Typescript we are used to using an object argument in order to know the arguments' names when using the function and also to not care about its order when passing arguments. something like:

function updateUser(userOptions) {
  ....
}

//using the function
updateUser({
  name: 'Diel',
  age: 26
})

In Rescript it is also possible to use an object, BUT, there is something called labeled arguments that is basically using arguments as we would use normally setting a name for it, and then when using the function it would be just a matter of using the name in any order. example:

let updateUser = (~name, ~age) => {
  ...
}

//using the function
updateUser(~age=26, ~name="Diel") // here you can set the arguments in any order

Remember all functions are curried by default? with labeled arguments we can create a new function using any arguments order we want, e.g:

let add = (~a, ~b) => a + b
let addTwoToA = add(~b=2)
let test = addTwoToA(~a=10)

It doesn't have null neither undefined

That is definitely great! We don't need to care about a whole category of bugs, however, the idea of a potentially nonexistent value is still useful and that is why Rescript has Option.

An Option can be represented by Some(value) or None variants, and whenever you need to use a variable from type Option. Rescript will force you to handle both cases due to its exhaustive pattern matching system.

For example, a user avatar is potentially nonexistent in many applications:

let userAvatar = Some("url...")

switch userAvatar {
| None => Js.log("The user doesn't have an avatar, let's show initials")
| Some(url) => Js.log("The user's avatar is " ++ url)
}

and if you forgot to handle one of the Option variants, the compiler would say:


Warning number 8
  /Users/dielduarte/localhost/testing-rescript/src/ExternalLibs.res:3:1-5:1

  1let userAvatar = Some("url...")
  2
  3switch userAvatar {
  4| Some(url) => Js.log("The user's avatar is " ++ url)
  5}

  You forgot to handle a possible case here, for example:
  None

>>>> Finish compiling 82 mseconds

😍 sometimes I feel like I would kiss the Rescript compiler.

Easy to use with any build tool

Since rescript compiles to JS, you can use Rescript with any build tool, create-react-app, Snowpack, Webpack, Babel, Rome, and more... anything that works for JS would work for Rescript because the idea is:

Rescript compiles to JS ⇒ then any tool you are using understands the JS files generated and starts working, they don't necessarily need to know you are using Rescript. To prove that, I created this template to use Rescript with snowpack you can check here:

dielduarte/react-snowpack-rescript-template

You can see that to start the snowpack dev server, I just imported the index.bs.js file generated by the Rescript compiler within the index.html file here

The end

As I told at the beginning of the post, the idea here was to talk about points that I considered important to take the decision of study more about the language, so I didn't cover many great things about the language and its benefits, yet, I hope it helps you to at least be interested in learning more!

and for you, does it worth it?

Diel's avatar, the image contains a border that will be full when the scroll of the page is done