DEV Community

Kevan Stannard
Kevan Stannard

Posted on

Thinking in ReScript

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;
}
Enter fullscreen mode Exit fullscreen mode

In ReScript this could be written as:

let x = if (someCondition) {
  0
} else {
  1
}
Enter fullscreen mode Exit fullscreen mode

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 };
};
Enter fullscreen mode Exit fullscreen mode

And the ReScript equivalent:

type point = {x: int, y: int}

let makePoint = (x, y) => {
  {x: x, y: y}
}
Enter fullscreen mode Exit fullscreen mode

"Function first" thinking

In object oriented programming it's common to see code like this:

const name = person.getName();
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Here's another example which passes some arguments to the function. In JavaScript we might write:

person.setName("First", "Last");
person.setId(1234);
Enter fullscreen mode Exit fullscreen mode

In ReScript we would write:

setName(person, "First", "Last")
setId(person, 1234)
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
}
Enter fullscreen mode Exit fullscreen mode

And in ReScript:

type person = {
  id: int,
  name: string,
}
Enter fullscreen mode Exit fullscreen mode

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,
  }
}
Enter fullscreen mode Exit fullscreen mode

So when declaring a person you might write it as:

let person: Person.t = {
  id: 1,
  name: "Laura"
}
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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")
}
Enter fullscreen mode Exit fullscreen mode

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")
Enter fullscreen mode Exit fullscreen mode

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)
  }
}
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

Which prints:

Mystery person
Person named Mira
Enter fullscreen mode Exit fullscreen mode

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 };
Enter fullscreen mode Exit fullscreen mode

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)
Enter fullscreen mode Exit fullscreen mode

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
})
Enter fullscreen mode Exit fullscreen mode

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
  })
Enter fullscreen mode Exit fullscreen mode

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>
  )
}
Enter fullscreen mode Exit fullscreen mode

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>
  }
}
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

Breaking this down:

  • @module("date-fns/format") refers to the module.

  • external format: indicates it's an external function and names the function format 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 the default 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.

Top comments (7)

Collapse
 
redbar0n profile image
Magne

Do you still use ReScript?

Collapse
 
kevanstannard profile image
Kevan Stannard

Yes, still using ReScript 👍

Collapse
 
redbar0n profile image
Magne

In the ReScript equivalent: Why didn't you have to associate 'makePoint' with the type 'point'? Was it inferred from naming convention?

Collapse
 
kevanstannard profile image
Kevan Stannard

Yes that's correct. ReScript does quite well with inferring types.

Collapse
 
thyecust profile image
thyecust

Hi, thanks for your great post and I made a Chinese Translation here: zhuanlan.zhihu.com/p/404476040

Collapse
 
kevanstannard profile image
Kevan Stannard

Amazing @thyecust , thank you

Collapse
 
logandeancall profile image
Logan Dean Call

Great job here, @kevanstannard . Thanks for writing this up! It's great to see articles targeted at helping people who are new to ReScript or who are considering it.