ReScript 2025 — The Top JavaScript Alternative (Tech Deep Dive)
TL;DR — ReScript gives you a sound nominal type system, lightning‑fast compiles (native OCaml backend), ergonomic ADTs + pattern matching, data‑first stdlib with pipes, and first‑class React support — without opting out of the JS ecosystem. If TypeScript is “JS with types”, ReScript is “types with JS interop”.
Why another language in the JS ecosystem?
Because large apps need:
- Type soundness: if it compiles, it won’t throw type errors at runtime.
-
Predictable data: immutability by default;
Record,Tuple,Variant. -
Expressive logic: pattern matching over ADTs;
Option/Resultergonomics. - Speed: native compiler; blazing feedback loop even at 50k+ files.
- Interop: consume NPM, emit human‑readable JS, bind to any library.
Type Soundness (Nominal > Structural)
TypeScript’s structural typing allows refutable assignments that can later crash. ReScript’s nominal types avoid this entire class.
// TypeScript — compiles, may crash at runtime
type T = { x: number }
type U = { x: number | string }
const a: T = { x: 3 }
const b: U = a // ✅ compiles
b.x = "string now"
a.x.toFixed(0) // 💥 at runtime, x might be string
/* ReScript — nominal types, won’t compile */
type t = { x: int }
type u = { x: int | string } // illustrative; unions usually use Variants
let a: t = { x: 3 }
/* let b: u = a // ❌ type error at compile time */
Result: fearless refactors. If it builds, you didn’t smuggle a time‑bomb through the type system.
Immutable by Default (with an escape hatch)
Records
type person = { age: int, name: string }
let me: person = { age: 24, name: "ReScript" }
let older = { ...me, age: me.age + 1 } // copy-on-write
Need mutability? Declare the field as mutable:
type person = { name: string, mutable age: int }
let p = { name: "Baby", age: 1 }
p.age = p.age + 1
Tuples
let ageAndName: (int, string) = (24, "Lil' ReScript")
type coord3d = (float, float, float)
Variants (ADTs)
type Account = Wechat(int, string) | Twitter({name: string, age: int})
type Animal = Dog | Cat | Bird
Pattern Matching (goodbye if/switch boilerplate)
type shape =
| Circle({radius: float})
| Square({x: float})
| Triangle({x: float, y: float})
let area = (s: shape) => switch s {
| Circle({radius}) => Js.Math._PI *. radius *. radius
| Square({x}) => x *. x
| Triangle({x, y}) => x *. y /. 2.0
}
Compiled JS stays readable (internally tagged), while you enjoy ADT ergonomics.
Null‑Safety via option (like Rust’s Option)
type option<'a> = None | Some('a)
let licenseNumber = Some(5)
switch licenseNumber {
| None => Js.log("No car")
| Some(n) => Js.log("License: " ++ Js.Int.toString(n))
}
Labeled Arguments & Pipes
Named parameters
let sub = (~first: int, ~second: int) => first - second
sub(~second=2, ~first=5) // 3
Pipe‑first by default
let add = (x,y) => x + y
let num = 1->add(5) // 6
let sum =
[1,2,3]
->Belt.Array.map(x => x + 2)
->Belt.Array.reduce(0, (acc, v) => acc + v)
Interop: Decorators & Extensions
External bindings
@module("path") external dirname: string => string = "dirname"
let root = dirname("/Leapcell/github")
Raw imports (no module import syntax)
%raw(`import "index.css";`)
React with @rescript/react
Define React components with types inferred from labeled props.
module Friend = {
@react.component
let make = (~name: string, ~children) => {
<div> {React.string(name)} children </div>
}
}
<Friend name="Leapcell"> {"hi"->React.string} </Friend>
JSX props like isLoading/text can be passed shorthand: <MyCmp isLoading text onClick />.
Async/await (since ReScript 10.1) and a Promise API in the new Core lib complete the modern DX.
The Compiler: Native Speed + Smart Output
- Constant folding: inlines trivial results.
- Type inference: context‑aware; most annotations optional.
- Type layout optimization: compact JS output via decorators.
let add = (x,y) => x + y
let num = add(5,3) // compiled JS -> `var num = 8;`
Ecosystem & Bindings
ReScript lives on NPM. Bindings exist for React, Node, Jotai, TanStack Query, RHF, MUI/AntD, Bun, Vitest, GraphQL, and more. Writing your own bindings is straightforward for the subset you actually use.
Migration Playbook (TS → ReScript)
- Start small: one leaf feature or a new route. Commit compiled JS alongside source.
- Create bindings only for the APIs you call (not entire libraries).
-
Model domain with ADTs (
Variant), kill “stringly‑typed” enums. -
Replace
null/undefinedwithoptionand pattern match. - Adopt pipes and data‑first stdlib to simplify chains.
-
Introduce React bindings via
@rescript/reactand keep pages/components incremental. - Perf‑test: enjoy fast compiles and instant IntelliSense on large codebases.
When ReScript shines
- Mission‑critical correctness: fintech, healthcare, infra dashboards.
- Large frontends: keep type feedback instant as teams/LOC grow.
-
Complex domain logic: model with
Variant, match exhaustively.
And yes, you still ship plain JavaScript that any tool can run.
Bonus: Deploy on Leapcell (serverless, async, Redis)
- Multi‑language: JS, Python, Go, Rust.
- Pay‑for‑usage: deploy unlimited projects free; $25 ≈ millions of requests.
- DX: GitOps pipelines, real‑time logs/metrics.
- Scaling: automatic concurrency, zero ops. Perfect for ReScript + React SSR/RSC.
Final Thoughts
TypeScript is great. ReScript is different: a sound type system, ADTs, match, pipes — with first‑class JS interop. If you’ve ever wanted “Rust‑style confidence in the JS world”, ReScript is the 2025 bet worth trying.
— Written by Cristian Sifuentes — Full‑stack developer & AI/JS enthusiast.

Top comments (3)
Typed errors and variant types are such an underused feature. Any language that has these gives developers super powers. By representing business logic as types, and showing what can go wrong inside of types, you allow super fast creation of features and easy refactoring.
An insightful deep dive into ReScript's type safety, pattern matching, and compilation speed benefits. Perfect for developers exploring reliable alternatives to JS/TS for large scale apps.
Great overview for those considering a transition to a more robust language.
Great write up!