ReasonML for JS developers

Authors
Jakub Kłobus
Software Engineer
@
Callstack

Let me ask you a few questions.

  • Are you a JavaScript developer? 
  • Are you annoyed by every undefined is not an object? 
  • Do you think static type checking makes your code slightly better? 
  • Currently, there is a hype for a ReasonML. Do you want to be on the bleeding edge?
  • Do you want to learn a new functional language? 
  • Are you tired of fixing production bugs?

If you answered "yes" to at least one of them, bear with me.

What is ReasonML?

ReasonML is a statically typed functional language. It bases on OCaml — a language created over 20 years ago. Reason combines the experience of OCaml (especially in the matter of types) and a fresh look with nice syntax similar to JavaScript. What is also important — it was built to be easily understandable for us — JS developers. Yey!

ReasonML logo

Why the heck should my JavaScript soul even look at ReasonML?

Good question. There are two main reasons. The first you will get to know after reading this article. The second is because you can. And you can because reason compiles extremely fast to JavaScript. Readable JS. It’s possible thanks to BuckleScript — OCaml/ReasonML compiler which transforms your Reason code into JavaScript. So in the end, you have JS code, which you can use wherever you would use classic JS — in the browser, in node.js or in React Native application. What’s also great is that you can still use your JavaScript tooling! Your favorite <rte-code>npm<rte-code> libraries are at your fingertips. The only thing you need are bindings to them and you’re ready to go. But this is a topic for a completely different article.

Tip: you can try all of the examples below in a sketch.sh. Just copy the example, paste it there, hit <rte-code>cmd+s<rte-code> (<rte-code>ctrl+s<rte-code>) and voila. You should know the right shortcut chain. It is what most of the developers (especially me) do at the daily basis — <rte-code>cmd+c<rte-code> <rte-code>cmd+v<rte-code> <rte-code>cmd+s<rte-code>.

Variables

Below you can find the same fragment of code written in JS (first example) and ReasonML (second example).

let myVar = "Hello";
view raw variables.js hosted with ❤ by GitHub

JS

let myVar = "Hello";
view raw variables.re hosted with ❤ by GitHub

ReasonML

Looks similar, right? Although there are two differences under the hood — in Reason there is only <rte-code>let<rte-code> keyword and that every variable is immutable which means you cannot change the value of the already created variable.

Tip: variables in Reason are immutable but you can bind totally new value to a variable with the same name. So <rte-code>let myVar<rte-code> in two different lines in the same scope isn't anything weird.

Types

Right now you are probably wondering why there are no type definitions in reason example above. Though you read a few seconds earlier that it is statically typed language. You’re right, I just forgot to mention that it is also extremely smart. Reason infers your types from your code so in most of the cases you don’t have to worry about them.

Of course, if you want you can specify types just like you do with <rte-code>flow<rte-code> or <rte-code>TypeScript<rte-code> - by adding semicolon proceeded with the type name after the variable name.

let myVariable: string = "Hello";
view raw types.js hosted with ❤ by GitHub

JS + flow — type definition

let myVariable: string = "Hello";
view raw types.re hosted with ❤ by GitHub

Reason — type definition

In Reason you can find most of the primitive types you probably already know:

  • string — <rte-code>let val: string = "Hi";<rte-code> (<rte-code>string<rte-code> in Flow)
  • int — <rte-code>let val: int = 12;<rte-code> (<rte-code>number<rte-code> in Flow)
  • float —<rte-code> let val: float= 12.2;<rte-code> (<rte-code>number<rte-code> in Flow)
  • bool — <rte-code>let val: bool = true;<rte-code> (<rte-code>boolean<rte-code> in Flow)
  • char — <rte-code>let val: string = 'a';<rte-code> (<rte-code>string<rte-code> in Flow)

Tip: in ReasonML different types require different operators. For example, <rte-code>+<rte-code> operator in JS could concatenate strings, add floats or integers. In Reason, you have to use <rte-code>++<rte-code> for strings, <rte-code>+<rte-code> for integers and +. for floats.

Objects (aka records)

In JS to group some data we use plain objects. In Reason it’s very similar, see the examples below.

let myObject = {
name: "Name",
age: 20,
};
view raw objects.js hosted with ❤ by GitHub

JS

type obj = {
name: string,
age: int
};
let myObject = {
name: "Name",
age: 20,
};
view raw records.re hosted with ❤ by GitHub

ReasonML

Nothing new here except that in Reason you have to define the type before you create an “object.” To do this you have to use <rte-code>type<rte-code> keyword and inside curly braces list all fields and their types. The syntax is familiar but there are more differences under the hood. The first difference is a name. What you see in the snippet is called a Record. The second difference is a fact that Reason’s records are immutable. You cannot reassign a field of an already created record. What you can only do is to create it in exactly the same shape as its type and then use it, nothing more.

Tip: Mutating records is forbidden but like in JS you can create a new record based on existing one using spread operator.

Functions

You probably use arrow functions on your daily basis. In ReasonML it is the only way to create a function.

let add = (a, b) => {
return a + b;
}
view raw function.js hosted with ❤ by GitHub

JS — function which adds two numbers

let add = (a, b) => {
a + b;
};
view raw function.re hosted with ❤ by GitHub

ReasonML — function which adds two numbers

The difference here — <rte-code>return<rte-code> keyword. In ReasonML everything is an expression. So the last line in each scope will be returned — <rte-code>return<rte-code> is not needed.

Tip: You can commit <rte-code>{<rte-code> brackets for function with only one line of code inside — just like in JS (suprise)

Tip 2: You can define the types of function arguments by adding <rte-code>:TypeName<rte-code> after the each argument name. Although it’s not required in most of the cases.
Example: <rte-code>let add = (a: int, b: int) => a + b;<rte-code>

Tip 3: You can also add labels to your arguments and call the function using argument’s labels instead of its order. They’re called labeled arguments.

Sequences of data

To start our discussion about sequences of data please look at the examples below.

let languages = ["ReasonML", "JS", "OCaml"];
view raw array.js hosted with ❤ by GitHub

JS — List

/* List */
let languages = ["ReasonML", "JS", "OCaml"];
/* Array */
let languages = [|"ReasonML", "JS", "OCaml"|];
view raw array.re hosted with ❤ by GitHub

ReasonML — List and Array

At first, JS and Reason versions look almost the same but in ReasonML example you can see two different syntaxes — one with <rte-code>[<rte-code> and another with <rte-code>[|<rte-code>. The first one is called a <rte-code>List<rte-code> and the second one is an <rte-code>Array<rte-code>. For us — JS enthusiasts — it looks a bit overcomplicated. Why differentiate Lists and Arrays? The answer is simple. Lists are immutable and fast at prepending items. You will probably use it for dynamic data structures. List of TODOs for TODO app will be a good place for using them.
Arrays are mutable (you can mutate each item), fast at random access & updates. If you’re looking for a JS-like experience and don’t have to add new items to array it’s the best choice for you.

When it comes to iterating over List or Array there are more differences between JS and Reason. Look, for example, at a simple <rte-code>map<rte-code>:

languages.map(l => l.toLowerCase());
view raw map.js hosted with ❤ by GitHub

JS — Iterating over an array

Array.map(l => String.lowercase_ascii(l), languages);
view raw map.re hosted with ❤ by GitHub

ReasonML — Iterating over an array

In JavaScript we have Array prototype with a bag of methods we can call directly on an array instance. There’s no such thing in ReasonML. But we have <rte-code>Array<rte-code> and <rte-code>List<rte-code> modules. Both are very similar and with an even bigger bag of useful functions like <rte-code>map<rte-code>, <rte-code>length<rte-code> or <rte-code>sort<rte-code>.

Tip: Accessing List items might be a little bit confusing at the first time so I recommend to start with an Array, learn more about pattern matching and recursion in Reason and than go back to Lists.

Modules

This will become a little bit crazy — But this is the level of craziness that everybody likes ;) Let’s compare JS with ReasonML by creating two similar modules that contain simple <rte-code>add<rte-code> function.

let add = (a, b) => a + b;
export add;
view raw AddModule.js hosted with ❤ by GitHub

JS — basic module

let add = (a, b) => a+b;
view raw AddModule.re hosted with ❤ by GitHub

ReasonML — basic module

First weirdness… There is no exporting in Reason. Each and every one of your files will become a module with the same name as the file.

Let’s move to the example of consuming the module. Imagine that we have file <rte-code>App<rte-code> directly in <rte-code>src/<rte-code> directory and we want to use add function from <rte-code>AddModule<rte-code> file in <rte-code>src/utils/<rte-code>. Here is how it looks like:

JS —using simple module

ReasonML — using simple module

Did I just forget to import the module in ReasonML example? No! No export — no import, easy. In ReasonML you can use your modules by using their names (in our example the filename). But what is more important — a location of the module doesn’t matter. You probably noticed in the example that <rte-code>AddModule<rte-code> file is in a different location than the <rte-code>App<rte-code> and we still were able to use it by simply calling its name. Pretty awesome, right? Just please remember to be careful with filenames to protect yourself from a name clashing.

Because using <rte-code>ModuleName<rte-code>. Everytime you want to use some modules function could be problematic ReasonML allows you to open the whole module for a specific scope. It basically means that you can make the content of a module visible inside some block of code — for example for the whole other module. To do this you have to type <rte-code>open ModuleName;<rte-code> at the beginning of a scope. So if you have some file (<rte-code>Math.re<rte-code>) with an add function inside and do <rte-code>open Math<rte-code>; at the beginning of another file you will be able to do <rte-code>add(1, 2)<rte-code> without any prefixes.

Tip: Be aware that opening modules globally could easily break your code. Situation when the module you’re opening has a function with the same name as some of yours functions or variables is more than possible. In those cases you can open module only locally. For example if you want to create a list with a few results of add function you can do the following: <rte-code>AddModule.[add(1, 2), add(2, 3), add(2, 4), add(2, 5)];<rte-code> You can find more details in the official ReasonML documentation or in ReasonML Hub article.

Variants

Imagine you have some blogging app abd you want to keep information about user review under every post. You would like to have three simple choices: <rte-code>Bad<rte-code>, <rte-code>Neutral<rte-code> and <rte-code>Awesome<rte-code>. Code which checks if the review is “not bad” could look like this:

let isNotBad = review => review !== 'Bad';
isNotBad('Neutral');
view raw Variant.js hosted with ❤ by GitHub

JS — variants/enums/options

type review = Bad | Neutral | Awesome;
let isNotBad = (review) => review !== Bad;
isNotBad(Neutral);
view raw Variant.re hosted with ❤ by GitHub

ReasonML — variants

Except for type definitions, both examples look very similar. The difference is that JS version is more error-prone. Each typo in the review name could cause a few hours of bug fixing. If you do the same typo in ReasonML compiler will tell you that you have a bug even before you start testing it.

But that’s not all. Imagine you want to keep some explanation text for the Bad option. Normally you would keep the data as a separated variable but ReasonML variants have one additional ability — constructors.

let handleOptionChange = (option, explanation) => {
// saving the option somewhere
if (option === "bad" && explanation) {
// saving explanation somewhere
}
}
// later in the code
handleOptionChange('bad', 'I hate ReasonML');

JS — adding the review with additional data

type review = Bad(string) | Neutral | Awesome;
let handleOptionChange = review => {
/* saving the review somewhere */
}
handleOptionChange(Bad("I hate ReasonML"));

ReasonML — adding the review with additional data (aka Variant Constructors)

The thing you see above is called a constructor. It’s a way to store some data inside a variant. This is an ideal way to store different types of data. Do you want to accept strings, integers or floats in some variable? Just create a variant: <rte-code>type data = Text(string) | Int(integer) | Fl(float);<rte-code>.

Tip: In a single variant, you can store more than one value. You can, for example, do the following: <rte-code>Point(float, float)<rte-code>.

Pipeline operator — chaining function calls

Calling function with the result of another function is something we do very often. Currently, in JS there is no better way than nesting function calls or assigning function result to some variable and than passing it to another one. In ReasonML there is a special operator that makes our lives easier — pipeline operator. You’ve probably heard this name before because it is quite common in other languages than JS. We even have a tc39 proposal with it.

let pow = a => a * a;
let result = pow(parseFloat('2.5'))
view raw chainedCalls.js hosted with ❤ by GitHub

JS — function calls chain

Well in Reason we already have it!

let pow = a => a *. a;
let result = "2.5" |> float_of_string |> pow;
view raw chainedCalls.re hosted with ❤ by GitHub

ReasonML — function calls chain

Like you see above we can use <rte-code>|><rte-code> to pass value on the left side of operator to the function on the right as its last argument. Extremaly simple syntax which makes the code much clearer.

Another great use case for the pipeline operators is the usage of modules like Array or List. When you were reading the paragraph about Lists you probably wondered how advanced use case would look like. In JavaScript it’s simple. Because we have a lot of useful methods on different object prototypes we can easily chain them. So this is a normal thing in JavaScript: <rte-code>array.filter(…).map(…)<rte-code>. Few of you probably remember the times when chaining <rte-code>jQuery<rte-code> selectors and functions were the most important part of every front-end developer’s life. It’s natural and clean but it’s not possible in ReasonML… but don’t worry, we have a pipeline operator… God bless the pipeline operator.

Below you can see the example of an advanced array modifying. The code filters out the 0 from an array, multiplies each value and converts it to a string.

let data = [1, 3, 5, 2, 0, 4];
let betterData = data
.filter(a => a !== 0)
.map(a => a * 2)
.map(a => a.toString());

JS — chaining Array prototype functions

let data = [1, 3, 5, 2, 0, 4 ];
data
|> List.filter(i => i != 0)
|> List.map(i => i * 2)
|> List.map(i => string_of_int(i));

ReasonML — chaining Array module function using pipe operator

Thanks to pipeline operator our ReasonML code looks almost the same as JS one. And what is more important there are no magic creatures like object instances, prototypes or <rte-code>this<rte-code>. There are just simple functions.

Pattern matching

If you’ve already heard some good opinions about ReasonML the most common thing that everybody loves about the language is Pattern Matching. What is it exactly? Pattern matching is like JS switch-case but on steroids.
Below we have an example of a function which returns some feedback for users after they left a review (we will use variants from the previous example).

let getAnswer = (review, message) => {
switch(review) {
case 'Bad':
return 'That is sad that you think: ' + message;
case 'Neutral':
return 'Okay';
case 'Awesome':
return 'Yes yes yes!';
};
}

JS — Switch statement (little brother of Pattern matching)

type review =
| Bad(string)
| Neutral
| Awesome;
let getAnswer = review =>
switch (review) {
| Bad(comment) => "That is sad that you think: " ++ comment
| Neutral => "Okay"
| Awesome => "Yes yes yes!"
};

ReasonML — Pattern matching

It is just a switch statement but prettier (no<rte-code>break<rte-code> or <rte-code>return<rte-code> keywords) , right? So, why everybody loves it so much? Look closer to the line with <rte-code>Bad<rte-code> variant. We just destructured it and assigned the value from <rte-code>variant<rte-code>constructor to <rte-code>comment<rte-code> variable. For those who don't know, destructuring is a way for easy extracting nested values of some variable - In ReasonML it could be a <rte-code>Record<rte-code>, an <rte-code>Array<rte-code> or for example a <rte-code>Variant constructor<rte-code> and it works exactly the same as in JS. We used destructuring of a Variant in pattern matching so analogically we can do it for any other type. We can match values in a very specific way and while doing it we can assign those values to variables — that’s why it’s called “Pattern Matching”. We match some patterns in our value.

Let’s see a little bit more exhaustive example of this powerful feature. After that, I hope you will either love Pattern matching or decide to spend more time with it. ;)

switch (userWithReview) {
| { name: "Maja", phone: "123123", review: Neutral, age: 24 } =>
"Here we match every value with name Maja, phone 123123 Neutral review and age 24"
| { name: "Maja", phone } =>
"Here we match every value with name Maja and we can use phone like a normal variable - " ++ phone
| { name, phone, review: Awesome} =>
"Here we match every user with review Awesome and we can use phone and name"
| { name, phone, review: Bad(explanation)} =>
"Here we match every user with review Baw and we can use phone, name, and the explanation"
| _ =>
"Here we match everything else"
};

ReasonML — advanced Pattern matching example

Just imagine how nested <rte-code>if<rte-code> statements hell would look like in JS after you implement the same logic… If you are brave enough you can see an example on GitHub.

Of course this is not everything. Maybe do you want to add some additional check to some of your matched values? Using when keyword, you can do something like that: <rte-code>| { name, phone, age } when age > 20 => “Matched every person above 20!”<rte-code>.

There is also one nice thing. Reason compiler will warn you if you won’t cover all cases from the Variant. Goodbye unhandled cases.

There are more parts of ReasonML

We went through the most interesting parts of ReasonML but there are much more — tuples, fast pipe operator, promises, advanced function currying, Objects, mutations, JS interop, native compilation etc.

To learn more I encourage you to look at some amazing tutorials, articles, or podcasts made by amazing people in the community:

If you liked the language after reading this article there is a homework for you — play a little with ReasonML, learn it, do something small and then conquer the world with it.

Insights

Learn more about JavaScript

cover

Jun 26Article

React Native Legal: CLI & Programmatic API - Unified Tooling for License Attributions

Discover React Native Legal, the first unified solution for managing open-source license attributions. Automatically handle both JS & native dependencies in any project via a powerful CLI and programmatic API.

cover

Jun 13Article

Manage Async Code Like a PRO With RxJS

Discover the Saviour for async code management in React with our blog post.

cover

Dec 12Article

JavaScript Hacking 101

In this article we takie a look at how things work in JavaScript under the hood.

cover

Aug 1Article

ECMAScript ES8 and Beyond

Quick introduction to ECMAScript proposals ES8 with the list of currently available presets and presentation of some features of each preset.

cover

Mar 19Tutorial

The Hard Way vs. The React Native AI SDK Way: Stop Writing Custom Modules for Every Model

Learn how React Native AI uses a unified provider model to switch between cloud and on-device LLMs without rewriting application logic, enabling reliable AI experiences online and offline.

cover

Mar 12Tutorial

Build Smarter Apps: Tool Calling & AI Orchestration Explained

Learn how on-device models in React Native can call tools, interact with external APIs, and return structured outputs you can use directly in application logic.

cover

Mar 5Tutorial

MLC LLM + React Native: On-Device AI Without the Pain

Learn how to run third-party on-device LLMs in React Native using MLC LLM. Choose your models, run them efficiently across platforms, and keep a unified JavaScript API with React Native AI.

cover

Feb 24Tutorial

Intelligent Fallbacks with Apple Intelligence in React Native

Learn how to use Apple’s built-in Foundation Models in React Native apps, reduce memory usage, and enable fast, fully on-device AI with React Native AI Apple.

cover

Feb 10Tutorial

What Is the React Native AI SDK? A Complete Intro & Quickstart

Learn why on-device AI matters for React Native apps, how local LLMs behave offline, and what problems React Native AI is designed to solve from day one.

cover

Jan 29Tutorial

Implementing an Android TurboModule with Kotlin

Learn how to implement a React Native TurboModule on Android, from the TypeScript spec and Codegen to the Kotlin implementation and package registration.

cover

Jan 19Tutorial

Working with Different Threads in Swift TurboModules

Learn how JavaScript, native module, background, and main threads interact in React Native TurboModules, and how to use GCD queues to run async work safely in Swift.

cover

Jan 9Tutorial

How to Add Type-Safe Constants to Swift TurboModules

Learn how to expose typed, immutable constants from a Swift TurboModule using Codegen, and get full end-to-end type safety between native and JavaScript.

cover

Dec 30Tutorial

Adding Event Emitters to Your TurboModule in Swift

Learn how to emit native events from a Swift TurboModule and subscribe to them in JavaScript, including proper memory management and Codegen integration.

cover

Dec 22Tutorial

Writing Your First TurboModule in Swift

Learn how to build a fully working TurboModule in Swift, integrate it with Codegen, and bridge it through Objective‑C inside an Expo app.