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
You can add type annotations if you would like.
// ReScript
let add = (a: int, b: int): int => a + b
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
}
// 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
}
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."),
}
}
// 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.")
}
}
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!"),
}
}
// 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!")
}
}
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?")
}
}
// rescript
let main = () => {
let n = Some("Josh")
switch n {
| Some(name) => Console.log(`Hello there ${name}!`)
| None => Console.log("Who are you?")
}
}
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),
}
}
// 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}`)
}
}
Learn more about ReScript
If you are interested in learning more about ReScript check out the official site or come over to the forum!