DEV Community

Cristian Sifuentes
Cristian Sifuentes

Posted on

ReScript 2025 — The Top JavaScript Alternative (Tech Deep Dive)

 ReScript 2025 — The Top JavaScript Alternative (Tech Deep Dive)

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
Enter fullscreen mode Exit fullscreen mode
/* 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 */
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

Tuples

let ageAndName: (int, string) = (24, "Lil' ReScript")
type coord3d = (float, float, float)
Enter fullscreen mode Exit fullscreen mode

Variants (ADTs)

type Account = Wechat(int, string) | Twitter({name: string, age: int})
type Animal = Dog | Cat | Bird
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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))
}
Enter fullscreen mode Exit fullscreen mode

Labeled Arguments & Pipes

Named parameters

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

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)
Enter fullscreen mode Exit fullscreen mode

Interop: Decorators & Extensions

External bindings

@module("path") external dirname: string => string = "dirname"
let root = dirname("/Leapcell/github")
Enter fullscreen mode Exit fullscreen mode

Raw imports (no module import syntax)

%raw(`import "index.css";`)
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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;`
Enter fullscreen mode Exit fullscreen mode

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)

  1. Start small: one leaf feature or a new route. Commit compiled JS alongside source.
  2. Create bindings only for the APIs you call (not entire libraries).
  3. Model domain with ADTs (Variant), kill “stringly‑typed” enums.
  4. Replace null/undefined with option and pattern match.
  5. Adopt pipes and data‑first stdlib to simplify chains.
  6. Introduce React bindings via @rescript/react and keep pages/components incremental.
  7. 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)

Collapse
 
hashbyt profile image
Hashbyt

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.

Collapse
 
zth profile image
Gabriel Nordeborn

Great write up!