Thinking in ReScript
Kevan Stannard
Posted on August 12, 2021
ReScript is a new language targeting JavaScript programmers. Particularly JavaScript programmers that have developed an interest in type safety with TypeScript or Flow.
ReScript feels very familiar due to having a JavaScript-like syntax, however there are some important differences. In this post I'll try provide an overview of some of the key differences that I hope will help you become productive more quickly when exploring ReScript.
Everything is an expression
In ReScript, everything is an expression that evaluates to a value, which includes if
and switch
statements.
For example, in JavaScript you could write something like:
let x;
if (someCondition) {
x = 0;
} else {
x = 1;
}
In ReScript this could be written as:
let x = if (someCondition) {
0
} else {
1
}
Functions don't need "return"
The last line that executes in a function becomes the return value, so an explicit return
statement is not needed.
In JavaScript we might write:
const makePoint = (x, y) => {
return { x: x, y: y };
};
And the ReScript equivalent:
type point = {x: int, y: int}
let makePoint = (x, y) => {
{x: x, y: y}
}
"Function first" thinking
In object oriented programming it's common to see code like this:
const name = person.getName();
This uses a subject - function pattern. Here person
is the subject and getName()
is the function.
ReScript is a functional programming language which reverses the order to function - subject. The example above would be written as:
let name = getName(person)
Here's another example which passes some arguments to the function. In JavaScript we might write:
person.setName("First", "Last");
person.setId(1234);
In ReScript we would write:
setName(person, "First", "Last")
setId(person, 1234)
This is such a common pattern in ReScript that a special syntax exists to support it.
ReScript provides a "pipe first" operator ->
which injects a value into the first argument of a function.
The setName()
and setId
code above could be re-written as:
person->setName("First", "Last")
person->setId(1234)
The pipe first concept so ubiquitous in ReScript that you'll likely see it in almost every ReScript code example.
Global modules
Each ReScript file is a module. There are two important concepts to understand.
First, if you create a ReScript file named Api.res
and it contains a function fetchUsers()
then ReScript automatically provides a global Api
module that you can access from anywhere. To call the function fetchUsers()
you would write:
Api.fetchUsers()
The second important concept to understand is that your directory structure has no impact on how you access modules. In other words your Api.res
file can be anywhere in your project but you would always access it as a global module named Api
.
A key consequence here is that every ReScript filename in a project must be unique. A common strategy is to use a naming convention to group related modules together, and generally keep your folder structure very shallow.
Here's an example folder/file structure:
/api
/Api_Users.res
/Api_Products.res
/components
/Component_Button.res
/Component_Tile.res
/pages
/Page_Index.res
/Page_Products.res
Type naming conventions
Type names in ReScript are always declared starting with a lower case letter, compared to TypeScript or Flow which uses capital letters.
You'll get a ReScript compiler error if you use the incorrect casing.
For example, in TypeScript:
type Person = {
id: number;
name: string;
}
And in ReScript:
type person = {
id: int,
name: string,
}
The type t
naming convention
When looking at example code you may frequently see a type declaration named t
. This is nothing special, it's simply a naming convention referring to the default type of a module.
For example:
module Person = {
type t = {
id: int,
name: string,
}
}
So when declaring a person you might write it as:
let person: Person.t = {
id: 1,
name: "Laura"
}
Variants
ReScript provides a concept called Variants.
These are similar to enums but are more powerful because they can also have arguments.
Here's an example:
// Declare a variant type
type choice = Yes | No | Maybe
// Declare a variable of that type, and set its value
let answer: choice = Yes
And then later we might use a switch
statement on that variable:
switch answer {
| Yes => Js.log("Answer is Yes")
| No => Js.log("Answer is No")
| Maybe => Js.log("Answer is Maybe")
}
Note some differences in syntax between a ReScript switch
and a JavsScript switch
.
Also, Js.log()
is the equivalent of console.log()
This example declares our own variant, but ReScript comes with some useful built-in variants as well.
The most common built-in variant is called option
which has the two values None
and Some
. In this case Some
is special because it has an argument which we'll show in an example below.
The option
type is used to represent having a value or not. In JavaScript terms, it's similar in concept to a variable having a value or being undefined.
Let's go though an example of using the option
type.
First declare some variables:
let nobody = None
let somebody = Some("Mira")
Here nobody
has the value None
indicating the absence of a value.
And somebody
has the value Some("Mira")
. You can think of Some
as a container holding the value Mira
.
Next, let's define a function to print these variable values.
let printName = (name: option<string>) => {
switch name {
| None => Js.log("Mystery person")
| Some(name) => Js.log("Person named " ++ name)
}
}
Notice this function takes one argument name: option<string>
. The type option<string>
means that name
will either contain the value None
or the value Some
with a string value.
In the switch
statement we can unwrap the Some
value to get access to its contents using | Some(name)
. Here the name
variable is the string Mira
.
And finally, we can call this function:
printName(nobody)
printName(somebody)
Which prints:
Mystery person
Person named Mira
Structural Types vs Nominal Types
TypeScript using structural types. This just means that it primarily looks at the shape of the types.
For example, in TypeScript we might write:
type Status =
| { type: "Idle"; }
| { type: "Processing"; id: number; }
const value: Status = { type: "Processing", id: 123 };
When TypeScript is processing the types it looks at the shape of the types. If it contains a type
property matching "Processing" and a number id
property then it matches the "Processing" type here.
In ReScript this would be written using variants:
type status =
| Idle
| Processing(int)
let value: status = Processing(123)
When ReScript is processing the types it uses nominal typing which means it looks at the name of the types.
Structural typing and nominal typing are different type systems which means that a type solution in TypeScript may require a different approach in ReScript.
Promises
Promises are such a common concept in modern web applications, it's worth briefly highlighting how the Promise syntax works in ReScript.
Note that here I'm using the rescript-promise library, which will soon become a part of the core language.
The promise functions I'll discuss here are:
Promise.then(promise, callback)
Promise.catch(promise, callback)
Promise.resolve()
Let's assume we have a function already written Api.getUsers()
that returns a promise.
We could write our ReScript code as follows:
let promise = Api.getUsers()
let promise = Promise.then(promise, users => {
Js.log(users) // Log the users
Promise.resolve() // Indicate we are done
})
let promise = Promise.catch(promise, error => {
Js.log(error) // Log the error
Promise.resolve() // Indicate we are done
})
Notice the "function first" concept in use here; then
and catch
accept a promise as the first argument, and also return a promise. We can use the pipe first operator described above to improve this syntax as follows:
let promise =
Api.getUsers()
->Promise.then(users => {
Js.log(users) // Log the users
Promise.resolve() // Indicate we are done
})
->Promise.catch(error => {
Js.log(error) // Log the error
Promise.resolve() // Indicate we are done
})
ReScript React
While discussing syntax, a brief mention about using React in ReScript.
React has type safe support in ReScript, including JSX syntax. There are a few syntax differences.
In JavaScript you might write a component like this:
function MyButton({ onClick, children }) {
return (
<button
className="my-button"
onClick={onClick}>
{children}
</button>
)
}
The ReScript equivalent would be:
module MyButton = {
@react.component
let make = (
~onClick: ReactEvent.Mouse.t => unit,
~children: React.element
) => {
<button
className="my-button"
onClick={onClick}>
{children}
</button>
}
}
The JSX is identical.
The module wrapping the component is just standard boilerplate for React components.
The make
function is a necessary part of the convention. All ReScript React components have this function.
Spend some time learning how to bind to external modules
Lastly, when working in TypeScript and installing an npm module you'll typically look for type definitions for that module.
One of the challenges of the TypeScript community is the effort required to create and maintain the type definitions of these modules.
ReScript encourages a different philosophy. While there are a number of bindings available, and for many packages they will be useful, there is no goal to create a comprehensive repository of bindings. Instead developers are encouraged to write their own bindings as they need them.
If you're coming from TypeScript this may seem surprising, but there are important reasons behind this recommendation (which are a bit detailed to go into in this post).
However, writing bindings are easy to learn and in reality take very little time.
For example, writing a binding to the date-fns library format
function might look like this:
@module("date-fns/format")
external format: (Js.Date.t, string) => string = "default"
Breaking this down:
@module("date-fns/format")
refers to the module.external format:
indicates it's an external function and names the functionformat
within ReScript.(Js.Date.t, string) => string
is the function signature; it takes a Date type and a format string as arguments, and returns a string.= "default"
means use thedefault
export from the module.
Once you get used to ReScript's binding syntax, it's quick to write and customise those bindings to your specific needs.
Conclusion
This post highlights some of the key differences I've found between JavaScript and ReScript.
However what I haven't highlighted here is how similar much of the syntax and thinking is. Once you feel comfortable with some of these differences above, then writing ReScript code feels just like writing JavaScript but with a slightly different mindset.
Lastly I've found that after spending some time with ReScript it's significantly influence and improved how I write type safe JavaScript.
I hope you found this post useful and that it helps in your exploration of ReScript.
Posted on August 12, 2021
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.