DEV Community

Cover image for Why Rescript is the Top JavaScript Alternative for 2025
Leapcell
Leapcell

Posted on

Why Rescript is the Top JavaScript Alternative for 2025

Image description

Leapcell: The Next - Gen Serverless Platform for Web Hosting, Async Tasks, and Redis

Introduction to ReScript

This language itself has many remarkable features, such as a more robust type system, more pure functional programming support, powerful language features, and a compiler written in a native language with extremely high performance. Of course, it also has corresponding disadvantages. This article will focus on introducing ReScript's powerful features, its surrounding ecosystem, and its integration with React, which is most closely related to our daily use.

Language Features

ReScript's syntax is not like TypeScript, which is a superset of JavaScript. It is quite different from JavaScript. We won't go into detail about the trivial syntax. Instead, we'll mainly list some typical features for introduction.

Type Sound

The meaning of "type sound" can be introduced by quoting a sentence from Wikipedia:

"If a type system is sound, then expressions accepted by that type system must evaluate to a value of the appropriate type (rather than produce a value of some other, unrelated type or crash with a type error)"

Simply put, a type system that passes compilation will not produce type errors at runtime. TypeScript is not type sound. You can see the reason from the following example:

// typescript
// This is a valid TypeScript code
type T = {
  x: number;
};

type U = {
  x: number | string;
};

const a: T = {
  x: 3
};

const b: U = a;

b.x = "i am a string now";

const x: number = a.x;

// error: x is string
a.x.toFixed(0);
Enter fullscreen mode Exit fullscreen mode

In ReScript, you won't be able to write code that passes type compilation but produces type errors at runtime. In the above example, TypeScript can compile because TypeScript is a structural type, while ReScript is a nominal type. The code const b: U = a; will not compile. Of course, this alone cannot guarantee type soundness. The specific proof process is quite academic, so we won't elaborate here.

The significance of type soundness lies in better ensuring the security of the project. Just like the advantage of TypeScript over JavaScript in large - scale projects, when the program scale becomes larger and larger, if the language you use is type sound, you can carry out fearless refactoring without worrying about runtime type errors after refactoring.

Immutable

Mutability often makes it difficult to track and predict data changes, which may lead to bugs. Immutability is an effective means to improve code quality and reduce bugs. JavaScript, as a dynamic language, has almost no support for immutability. TC39 also has a related proposal for Record & Tuple, which is currently in stage 2. ReScript already has these two data types built - in.

Record

The main differences between ReScript's record and JavaScript's object are as follows:

  • Immutable by default.
  • A corresponding type must be declared when defining a record.
// rescript
type person = {
  age: int,
  name: string,
}

let me: person = {
  age: 5,
  name: "Big ReScript"
}

// Update the age field
let meNextYear = {...me, age: me.age + 1}
Enter fullscreen mode Exit fullscreen mode

ReScript also provides an escape hatch for mutable updates of specific record fields:

// rescript
type person = {
  name: string,
  mutable age: int
}

let baby = {name: "Baby ReScript", age: 5}

// Update the age field
baby.age = baby.age + 1
Enter fullscreen mode Exit fullscreen mode

Tuple

TypeScript also has the tuple data type. The only difference in ReScript's tuple is that it is immutable by default.

let ageAndName: (int, string) = (24, "Lil' ReScript")
// a tuple type alias
type coord3d = (float, float, float)
let my3dCoordinates: coord3d = (20.0, 30.5, 100.0)

// Update the tuple
let coordinates1 = (10, 20, 30)
let (c1x, _, _) = coordinates1
let coordinates2 = (c1x + 50, 20, 30)
Enter fullscreen mode Exit fullscreen mode

Variant

Variant is a rather special data structure in ReScript, covering most data modeling scenarios, such as enumerations and constructors (ReScript has no concept of class).

// rescript

// Define an enumeration
type animal = Dog | Cat | Bird

// Constructor, can pass any number of parameters or directly pass a record
type account = Wechat(int, string) | Twitter({name: string, age: int})
Enter fullscreen mode Exit fullscreen mode

Combined with other features of ReScript, variant can achieve powerful and elegant logical expression capabilities, such as pattern matching, which will be discussed next.

Pattern Matching

Pattern matching is one of the most useful features in programming languages. When combined with ADT (Algebraic Data Type), its expressive power is much better than traditional if & switch statements. It can not only judge values but also the specific type structure. JavaScript also has a related proposal, but it is only in stage 1, and it is still a long way from being truly usable. Before introducing this powerful feature, let's first look at an example of TypeScript's discriminated union:

// typescript
// tagged union
type Shape =
  | { kind: "circle"; radius: number }
  | { kind: "square"; x: number }
  | { kind: "triangle"; x: number; y: number };

function area(s: Shape) {
  switch (s.kind) {
    case "circle":
      return Math.PI * s.radius * s.radius;
    case "square":
      return s.x * s.x;
    default:
      return (s.x * s.y) / 2;
  }
}
Enter fullscreen mode Exit fullscreen mode

In TypeScript, when we want to distinguish the specific type of a union type, we need to manually add a kind string tag to distinguish. This form is relatively cumbersome. Next, let's see how ReScript handles this form:

// rescript
type shape =
  | Circle({radius: float})
  | Square({x: float})
  | Triangle({x: float, y: float})

let area = (s: shape) => {
  switch s {
    // ReScript's arithmetic operators for floating - point numbers need to add a dot, e.g., +., -., *.
    | Circle({radius}) => Js.Math._PI *. radius *. radius
    | Square({x}) => x *. x
    | Triangle({x, y}) => x *. y /. 2.0
  }
}

let a = area(Circle({radius: 3.0}))
Enter fullscreen mode Exit fullscreen mode

By combining variant to construct a sum type and then using pattern matching to match the specific type and deconstruct the attributes, there is no need to manually add tags. The writing style and experience are much more elegant. The compiled JavaScript code actually also uses tags to distinguish, but through ReScript, we can enjoy the benefits brought by ADT and pattern matching.

// Compiled JavaScript code
function area(s) {
  switch (s.TAG | 0) {
    case /* Circle */0 :
        var radius = s.radius;
        return Math.PI * radius * radius;
    case /* Square */1 :
        var x = s.x;
        return x * x;
    case /* Triangle */2 :
        return s.x * s.y / 2.0;

  }
}

var a = area({
      TAG: /* Circle */0,
      radius: 3.0
    });
Enter fullscreen mode Exit fullscreen mode

NPE

For the NPE problem, TypeScript can now effectively solve it through strictNullCheck and optional chaining. ReScript does not have null and undefined types by default. For cases where data may be empty, ReScript uses the built - in option type and pattern matching to solve the problem, similar to Rust. First, let's look at the definition of ReScript's built - in option type:

// rescript
// 'a represents a generic type
type option<'a> = None | Some('a)
Enter fullscreen mode Exit fullscreen mode

Using pattern matching:

// rescript
let licenseNumber = Some(5)

switch licenseNumber {
| None =>
  Js.log("The person doesn't have a car")
| Some(number) =>
  Js.log("The person's license number is " ++ Js.Int.toString(number))
}
Enter fullscreen mode Exit fullscreen mode

Labeled Arguments

Labeled arguments are actually named parameters. JavaScript itself does not support this feature. Usually, when there are many function parameters, we use object deconstruction to implement a poor - man's version of named parameters.

const func = ({
    a,
    b,
    c,
    d,
    e,
    f,
    g
})=>{

}
Enter fullscreen mode Exit fullscreen mode

The unfriendly part of this method is that we need to write a separate type declaration for the object, which is quite cumbersome. Next, let's see what ReScript's syntax looks like:

// rescript

let sub = (~first: int, ~second: int) => first - second
sub(~second = 2, ~first = 5) // 3

// alias
let sub = (~first as x: int, ~second as y: int) => x - y
Enter fullscreen mode Exit fullscreen mode

Pipe

There is also a proposal for the pipe operator in JavaScript, which is currently in stage 2. The pipe operator can relatively elegantly solve the problem of nested function calls, avoiding code like validateAge(getAge(parseData(person))). ReScript's pipe is pipe first by default, that is, it pipes to the first parameter of the next function.

// rescript

let add = (x,y) => x + y
let sub = (x,y) => x - y
let mul = (x,y) => x * y

// (6 - 2)*3 = 12
let num1 = mul(sub(add(1,5),2),3)
let num2 = add(1,5)
            ->sub(2)
            ->mul(3)
Enter fullscreen mode Exit fullscreen mode

Usually, in JavaScript, we use method chaining to optimize nested function calls, as shown below:

// typescript

let array = [1,2,3]
let num = array.map(item => item + 2).reduce((acc,cur) => acc + cur, 0)
Enter fullscreen mode Exit fullscreen mode

It is worth mentioning that ReScript does not have classes, so there is no such thing as class methods, and there will be no method chaining. Many built - in standard libraries in ReScript (such as map and reduce for arrays) are designed with a data - first approach and the pipe operator to achieve the method chaining that we are familiar with in JavaScript.

// rescript

// Example of using map and reduce in the ReScript standard library
Belt.Array.map([1, 2], (x) => x + 2) == [3, 4]
Belt.Array.reduce([2, 3, 4], 1, (acc, value) => acc + value) == 10

let array = [1,2,3]
let num = array
            -> Belt.Array.map(x => x + 2)
            -> Belt.Array.reduce(0, (acc, value) => acc + value)
Enter fullscreen mode Exit fullscreen mode

Decorator

ReScript's decorator is not used for metaprogramming in classes like TypeScript's. It has some other uses, such as for some compilation features and interoperating with JavaScript. In ReScript, you can import a module and define its type as follows:

// rescript

// Reference the dirname method of the path module and declare its type as string => string
@module("path") external dirname: string => string = "dirname"
let root = dirname("/Leapcell/github") // returns "Leapcell"
Enter fullscreen mode Exit fullscreen mode

Extension Point

Similar to the decorator, it is also used to extend JavaScript, but the syntax is a bit different. For example, in front - end development, we usually import CSS, and the build tool will handle it accordingly. However, ReScript's module system does not have an import statement and does not support importing CSS. In this case, we usually use %raw.

// rescript
%raw(`import "index.css";`)

// The output content of the compiled JavaScript
import "index.css";
Enter fullscreen mode Exit fullscreen mode

React Development

JSX

ReScript also supports JSX syntax, but there are some differences in props assignment:

// rescript

<MyComponent isLoading text onClick />
// Equivalent to
<MyComponent isLoading={isLoading} text={text} onClick={onClick} />
Enter fullscreen mode Exit fullscreen mode

@rescript/react

The @rescript/react library mainly provides ReScript bindings for React, including react and react - dom.

// rescript

// Define a React component
module Friend = {
  @react.component
  let make = (~name: string, ~children) => {
    <div>
      {React.string(name)}
      children
    </div>
  }
}
Enter fullscreen mode Exit fullscreen mode

ReScript provides the @react.component decorator for defining React components. The make function is the specific implementation of the component, which uses labeled arguments to get props. The Friend component can be directly used in JSX.

// rescript

<Friend name="Leapcell" age=20 />

// ReScript code after removing JSX syntactic sugar
React.createElement(Friend.make, {name: "Leapcell", age:20})
Enter fullscreen mode Exit fullscreen mode

At first glance, the make function seems a bit redundant, but this is due to some historical design reasons, so we won't go into too much detail here.

Ecosystem

Integrating into the JS Ecosystem

One of the key factors for the success of a JavaScript dialect is how to integrate with the existing JavaScript ecosystem. One of the reasons why TypeScript is so popular is that it is easy to reuse existing JavaScript libraries. You only need to write good d.ts files, and a TypeScript project can smoothly import and use them. In fact, ReScript is similar. You only need to declare the relevant ReScript types for JavaScript libraries. Take @rescript/react as an example. This library provides ReScript type declarations for React. Let's see how to declare the type for React's createElement:

// rescript

// ReactDOM.res
@module("react-dom")
external render: (React.element, Dom.element) => unit = "render"
// Bind the render function to the react - dom library
// In ReScript's module system, each file is a module, and the module name is the file name. There is no need to import, so you can directly use ReactDOM.render

let rootQuery = ReactDOM.querySelector("#root")
switch rootQuery {
  | Some(root) => ReactDOM.render(<App />, root)
  | None => ()
}
Enter fullscreen mode Exit fullscreen mode

Powerful Compiler

TypeScript's compiler is written in Node.js, and its compilation speed has always been criticized. Therefore, there are TypeScript compilers like esbuild and swc that only perform type erasure, but they still cannot meet the need for type checking. So, the stc project (TypeScript type checker written in Rust) has also attracted much attention. ReScript doesn't have many worries about this issue. ReScript's compiler is implemented in the native language OCaml, and compilation speed will not be a problem that ReScript projects need to worry about and solve. In addition, ReScript's compiler has many features. Since there is no detailed documentation on this aspect, here we only list a few features that I have a little understanding of.

Constant Folding

Constant folding means calculating the value of a constant expression and embedding it as a constant in the final generated code. In ReScript, common constant expressions and simple function calls can all be subject to constant folding.

let add = (x,y) => x + y
let num = add(5,3)

// Compiled JavaScript
function add(x, y) {
  return x + y | 0;
}

var num = 8;
Enter fullscreen mode Exit fullscreen mode

The compilation result of the same code in TypeScript is as follows:

// typescript
let add = (x:number,y:number)=>x + y
let num = add(5,3)

// Compiled JavaScript
"use strict";
let add = (x, y) => x + y;
let num = add(5, 3);
Enter fullscreen mode Exit fullscreen mode

Type Inference

TypeScript also has type inference, but ReScript's is more powerful. It can perform context - based type inference. In most cases, you hardly need to declare types for variables when writing ReScript code.

// rescript

// Fibonacci sequence, rec is used to declare a recursive function
let rec fib = (n) => {
  switch n {
    | 0 => 0
    | 1 => 1
    | _ => fib(n - 1) + fib(n - 2)
  }
}
Enter fullscreen mode Exit fullscreen mode

In the above Fibonacci sequence function implemented in ReScript, there is no variable declaration, but ReScript can infer that n is of type int from the context of pattern matching. In the same example, TypeScript must declare the number type for n.

// typescript

// Parameter 'n' implicitly has an 'any' type.
let fib = (n) => {
  switch (n) {
    case 0:
      return 0;
    case 1:
      return 1;
    default:
      return fib(n - 1) + fib(n - 2)
  }
}
Enter fullscreen mode Exit fullscreen mode

Type Layout Optimization

One of the functions of type layout optimization is to optimize the code size. For example, declaring an object requires more code than declaring an array.

let a = {width: 100, height: 200}

let b = [100,200]

// After uglification

let a={a:100,b:100}
let b=[100,200]

Enter fullscreen mode Exit fullscreen mode

In the above example, the readability of object declarations cannot be replaced by arrays. In daily use, we won't sacrifice code maintainability for this kind of optimization. In ReScript, through the decorator mentioned above, we can maintain readability when writing code, and the compiled JavaScript can also optimize the code size.

type node = {@as("0") width : int , @as("1") height : int}
let a: node = {width: 100,height: 200}

// Compiled JavaScript

var a = [
  100,
  200
];
Enter fullscreen mode Exit fullscreen mode

As a unique JavaScript dialect, ReScript has its own advantages in terms of type system, language features, integration with React, and ecosystem integration. Its powerful compiler also brings many conveniences to development. Although in the current environment where TypeScript is popular, ReScript may still be relatively niche, the features it possesses are worth developers' in - depth understanding and exploration, and it may bring new ideas and solutions to project development.

Leapcell: The Next - Gen Serverless Platform for Web Hosting, Async Tasks, and Redis

Finally, let me introduce to you a platform that is most suitable for deploying web services: Leapcell

Image description

1. Multi - Language Support

  • Develop with JavaScript, Python, Go, or Rust.

2. Deploy unlimited projects for free

  • Pay only for usage — no requests, no charges.

3. Unbeatable Cost Efficiency

  • Pay - as - you - go with no idle charges.
  • Example: $25 supports 6.94M requests at a 60ms average response time.

4. Streamlined Developer Experience

  • Intuitive UI for effortless setup.
  • Fully automated CI/CD pipelines and GitOps integration.
  • Real - time metrics and logging for actionable insights.

5. Effortless Scalability and High Performance

  • Auto - scaling to handle high concurrency with ease.
  • Zero operational overhead — just focus on building.

Image description

Explore more in the documentation!

Leapcell Twitter: https://x.com/LeapcellHQ

Top comments (0)