Rescript: The Best JavaScript Alternative in 2025
Daniel Hayes
Full-Stack Engineer · Leapcell

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);
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}
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
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)
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})
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; } }
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}))
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 });
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)
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)) }
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 })=>{ }
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
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)
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)
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)
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"
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";
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} />
@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> } }
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})
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 => () }
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;
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);
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) } }
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) } }
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]
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 ];
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
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.
Explore more in the documentation!
Leapcell Twitter: https://x.com/LeapcellHQ