ReScript: Rust like features for JavaScript

Josh Derocher-Vlk - Jan 18 - - Dev Community

If Rust is "C++ in ML clothing" then ReScript is "JavaScript in ML clothing".

What is ReScript?

ReScript is "Fast, Simple, Fully Typed JavaScript from the Future". What that means is that ReScript has a lightning fast compiler, an easy to learn JS like syntax, strong static types, with amazing features like pattern matching and variant types. Until 2020 it was called "BuckleScript" and is closely related to ReasonML.

ReScript is growing and adding features to make it more appealing as an alternative to JavaScript. ReScript v11 was recently released and adds some very nice quality of life improvements.

Let's take a quick look at a few features that Rust and ReScript share with some code examples.

Types

Rust and ReScript are both strong, statically-typed languages with roots in OCaml. Everything has a type and that type is guaranteed to be correct. Unlike TypeScript you won't find the any type in ReScript.

ReScript relies heavily on type inference which helps remove some clutter from type annotations.

// Typescript
let add = (a, b) => a + b // infers (any, any) => any

// ReScript
let add = (a, b) => a + b // infers (int, int) => int
Enter fullscreen mode Exit fullscreen mode

You can add type annotations if you would like.

// ReScript
let add = (a: int, b: int): int => a + b
Enter fullscreen mode Exit fullscreen mode

Expression based

Rust and ReScript are expression based languages, which means most lines of code or blocks of code return a value. You also don't have to write an explicit return for an expression, the last value in a block is the value returned.

// rust
fn main() {
    let y = {
        let x = 3;
        x + 1
    };

    println!("The value of y is: {y}"); // the value of y is: 4
}
Enter fullscreen mode Exit fullscreen mode
// rescript
let main = () => {
  let y = {
    let x = 3
    x + 1
  }

  Console.log(`The value of y is: ${y->Int.toString}`) // the value of y is: 4
}
Enter fullscreen mode Exit fullscreen mode

Pattern Matching

Pattern matching is a powerful way to do something conditionally based on types or data. You can think of it as a super powered switch statement that can match on the type or the value of the data. Rust and ReScript will give you a compiler error if you fail to handle all possible cases, which makes refactoring much easier to handle. If you ever have to add a new case you'll know exactly where to go in the code to make sure you handle it correctly.

// Rust
fn main() {
    let n = 42;

    match n {
        10 => println!("The number ten."),
        42 => println!("Answer to the Ultimate Question of Life, the Universe, and Everything."),
        _ => println!("Some other number."),
    }
}
Enter fullscreen mode Exit fullscreen mode
// rescript
let main = () => {
  let x = 42
  switch x {
  | 10 => Console.log("The number ten.")
  | 42 =>
    Console.log(
      "Answer to the Ultimate Question of Life, the Universe, and Everything.",
    )
  | _ => Console.log("Some other number.")
  }
}
Enter fullscreen mode Exit fullscreen mode

Tagged Unions and Variant types

Rust's tagged unions and ReScript variant types give you a way to not only create a type, but to also give that type data. This can be combined with pattern matching to make sure you handle all possible cases.

This article I wrote shows how you can use ReScript's variant types to connect business logic to the compiler.

// Rust
enum Widget {
    Red(u8),
    Blue(u8),
    None,
}

fn main() {
    let x = Widget::Red(42);

    match x {
        Widget::Red(_) => println!("You have a red widget!"),
        Widget::Blue(_) => println!("You have a blue widget!"),
        Widget::None => println!("You have no widget!"),
    }
}
Enter fullscreen mode Exit fullscreen mode
// ReScript
type widget =
  | Red(int)
  | Blue(int)
  | None

let main = () => {
  let x = Red(42)

  switch x {
  | Red(_) => Console.log("You have a red widget!")
  | Blue(_) => Console.log("You have a blue widget!")
  | None => Console.log("You have no widget!")
  }
}
Enter fullscreen mode Exit fullscreen mode

Option instead of undefined or null

Not having to deal with null or undefined errors saves so much time and prevents errors and headaches in production. Option forces you to explicitly deal with anything that might not be defined and having it typed this way makes it clear when you expect something to be undefined.

// Rust
fn main() {
    let n = Some("Josh");

    match n {
        Some(name) => println!("Hello there {:?}!", name),
        None => println!("Who are you?")
    }
}
Enter fullscreen mode Exit fullscreen mode
// rescript
let main = () => {
  let n = Some("Josh")

  switch n {
  | Some(name) => Console.log(`Hello there ${name}!`)
  | None => Console.log("Who are you?")
  }
}
Enter fullscreen mode Exit fullscreen mode

Result instead of throwing errors

Both languages have the Result type which can represent a response from an expression that might have failed. It's useful to handle expected errors without blowing up the program. Expected errors could be things like an api timing out, or a 400 response for a bad request. It allows the program to track information about an error and handle it gracefully without throwing an exception.

// rust
fn main() {
    let n: Result<&str, &str> = Ok("Josh");

    match n {
        Ok(name) => println!("Hello there {:?}!", name),
        Err(err) => println!("Error: {:?}", err),
    }
}
Enter fullscreen mode Exit fullscreen mode
// rescript
let main = () => {
  let n: result<string, string> = Ok("Josh")

  switch n {
  | Ok(name) => Console.log(`Hello there ${name}`)
  | Error(err) => Console.log(`Error: ${err}`)
  }
}
Enter fullscreen mode Exit fullscreen mode

Learn more about ReScript

If you are interested in learning more about ReScript check out the official site or come over to the forum!

. . . . . . . . .