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
/Result
ergonomics. - 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/undefined
withoption
and pattern match. - Adopt pipes and data‑first stdlib to simplify chains.
-
Introduce React bindings via
@rescript/react
and 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 (2)
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!