ReScript: Fast Type-Safe JavaScript Alternative
ReScript is a modern language that compiles to highly‑optimized JavaScript while preserving a strict type system. If you’ve ever felt the pain of runtime type errors in a JavaScript project, ReScript offers a refreshing alternative that catches bugs at compile time without sacrificing performance. Its syntax is concise, its tooling is fast, and it integrates seamlessly with existing JavaScript ecosystems, making it an attractive choice for teams looking to boost productivity and reliability.
Why ReScript Stands Out
First and foremost, ReScript’s type inference is both powerful and approachable. You get the safety of a statically‑typed language without the verbosity of languages like TypeScript. The compiler runs in milliseconds, even on large codebases, because it focuses on a single output target: clean, readable JavaScript.
Second, the generated JavaScript is intentionally minimal. ReScript avoids polyfills and heavy runtime helpers, which means the bundle size stays low and the code runs as fast as hand‑written JavaScript. This is a big win for performance‑critical applications such as SPAs, real‑time dashboards, or mobile web apps.
Finally, ReScript embraces a “no‑magic” philosophy. The language surface is small, the build pipeline is straightforward, and the interop model with JavaScript is explicit. You can call any JS function from ReScript and vice‑versa without fiddling with complex type declarations.
Getting Started in Minutes
Setting up a ReScript project is as simple as running a single command. The official bsb (BuckleScript) tool handles scaffolding, compilation, and watch mode out of the box.
npm init -y
npm install --save-dev rescript
npx rescript init
npm run start # starts the watch compiler
After the initial setup, you’ll find a src folder with a Demo.res file. Replace its content with the example below to see ReScript in action.
Example 1: A Simple Calculator
This example demonstrates ReScript’s type safety and pattern matching. The calc function takes an operator and two numbers, then returns the computed result. Any misuse—such as passing a string where a number is expected—will be flagged at compile time.
let add = (a: int, b: int): int => a + b
let sub = (a: int, b: int): int => a - b
let mul = (a: int, b: int): int => a * b
let div = (a: int, b: int): option(int) =>
if b == 0 {
None
} else {
Some(a / b)
}
type operator =
| Add
| Sub
| Mul
| Div
let calc = (op: operator, x: int, y: int): option(int) =>
switch op {
| Add => Some(add(x, y))
| Sub => Some(sub(x, y))
| Mul => Some(mul(x, y))
| Div => div(x, y)
}
let result = calc(Add, 10, 5)
switch result {
| Some(v) => Js.log2("Result:", v)
| None => Js.log("Division by zero!")
}
Notice the explicit option(int) return type for division. This forces the caller to handle the “division by zero” case, eliminating a whole class of runtime crashes.
Pro tip: UseoptionandResulttypes to model error conditions instead of throwing exceptions. This makes your code more predictable and easier to test.
Seamless Interop with Existing JavaScript
One of the biggest concerns when adopting a new language is how well it plays with the existing JavaScript codebase. ReScript’s @bs.module and @bs.val annotations let you import any JS library with zero friction.
Example 2: Using the Fetch API
The following snippet shows how to wrap the native fetch function in a type‑safe ReScript API. The wrapper returns a Promise that resolves to a typed JSON object, so you never have to cast any at runtime.
[@bs.val] external fetch: string => Js.Promise.t(Js.Json.t) = "fetch"
type user = {
id: int,
name: string,
email: string,
}
/* Decode JSON safely */
let decodeUser = (json: Js.Json.t): option(user) => {
let open Js.Json in
switch (json |> decodeObject) {
| Some(dict) =>
switch (
dict["id"] |> decodeNumber,
dict["name"] |> decodeString,
dict["email"] |> decodeString
) {
| (Some(id), Some(name), Some(email)) =>
Some({id: int_of_float(id), name, email})
| _ => None
}
| None => None
}
}
/* High‑level fetch wrapper */
let fetchUser = (url: string): Js.Promise.t(option(user)) =>
fetch(url)
|> Js.Promise.then_(json => Js.Promise.resolve(decodeUser(json)))
fetchUser("https://api.example.com/user/42")
|> Js.Promise.then_(userOpt =>
switch userOpt {
| Some(user) => Js.log2("Fetched user:", user)
| None => Js.log("Failed to decode user data")
})
|> ignore
Because the JSON decoding is typed, you get compile‑time guarantees that the fields you access actually exist. If the API changes, the compiler will immediately alert you, preventing silent bugs.
Building UI with ReScript‑React
ReScript isn’t just for backend logic; its React bindings are first‑class citizens. The rescript-react library provides a JSX‑like syntax that feels natural to JavaScript developers while keeping the type safety you love.
Example 3: A Counter Component
Below is a minimal counter component built with ReScript‑React. The component’s state is a simple integer, and the click handler is fully typed, ensuring you never accidentally pass a non‑numeric value.
open React
[@react.component]
let make = () => {
let (count, setCount) = React.useState(() => 0)
let increment = () => setCount(prev => prev + 1)
let decrement = () => setCount(prev => prev - 1)
}
When this component is compiled, the output JavaScript is clean and free of the extra runtime that TypeScript’s JSX transpilation often adds. The result is a lightweight bundle that loads quickly on the client.
Pro tip: Enable--genTypein yourbsconfig.jsonto generate TypeScript declaration files automatically. This lets you share ReScript components with pure TypeScript projects without losing type safety.
Real‑World Use Cases
1. Large‑scale SPAs – Companies building single‑page applications benefit from ReScript’s fast compiler and tiny runtime. The type system prevents the dreaded “undefined is not a function” errors that often plague JavaScript codebases as they grow.
2. Serverless Functions – ReScript’s output is plain JavaScript, making it a perfect fit for AWS Lambda, Cloudflare Workers, or Vercel serverless functions. You write concise, type‑checked code, and the deployment artifact is just a single .js file.
3. Data‑Intensive Dashboards – When handling real‑time streams or complex data transformations, ReScript’s pattern matching and immutable data structures simplify reasoning about state changes, reducing bugs in critical visualizations.
Performance Benchmarks
Benchmarks conducted by the ReScript community show that the compiled output often outperforms equivalent TypeScript code. The reasons are twofold: ReScript’s compiler emits native JavaScript constructs instead of helper functions, and its optimizer eliminates dead code aggressively.
- **Bundle size:** ReScript typically adds <10 KB* to a project, whereas TypeScript’s helper libraries can push the size past 30 KB.
- **Execution speed:** In a simple loop benchmark, ReScript’s output ran ~15 % faster than the same logic written in TypeScript and compiled with tsc.
- **Compile time:** On a 200 KB codebase, ReScript compiled in under 200 ms, while tsc took roughly 1.2 seconds.
*Numbers are approximate and vary with project complexity.
Best Practices for a Smooth Adoption
Transitioning a JavaScript team to ReScript can be done incrementally. Start by writing new modules in ReScript and gradually replace legacy files. Keep the following guidelines in mind:
- Use
@bs.modulefor external libs. This isolates the interop surface and lets the compiler enforce correct usage. - Prefer immutable data. ReScript’s standard library encourages immutable structures, which align well with React’s state management.
- Leverage
Resultfor error handling. This pattern integrates nicely with async code and avoids unchecked exceptions. - Enable source maps. The compiler can emit source maps that map the generated JavaScript back to the original ReScript files, making debugging a breeze.
Pro tip: Run npx rescript format as a pre‑commit hook. Consistent formatting reduces friction when code reviews involve both ReScript and JavaScript files.
Tooling and Ecosystem
ReScript ships with a dedicated language server (LSP) that provides autocompletion, inline type hints, and quick‑fix suggestions in editors like VS Code, Neovim, and Emacs. The ecosystem includes bindings for popular libraries such as react, express, and node-fetch, all maintained under the @rescript namespace.
Package management follows the familiar npm workflow, so you can install third‑party JS packages without any extra configuration. When a library lacks a ReScript binding, you can write a thin wrapper using @bs.val or @bs.module in just a few lines.
Common Pitfalls and How to Avoid Them
Misunderstanding option vs. Result – option is great for “maybe” values, but when you need an error message, Result provides a richer context. Choose the right type to keep error handling expressive.
Overusing external JS APIs – While interop is easy, leaning too heavily on untyped JavaScript can erode the safety guarantees ReScript offers. Wrap external calls in typed abstractions as soon as possible.
Ignoring compiler warnings – ReScript’s compiler is intentionally noisy about potential issues. Treat warnings as errors; they often hint at subtle bugs that would surface at runtime in plain JavaScript.
Future Roadmap
The ReScript team is actively working on improving ergonomics and expanding the standard library. Upcoming features include better support for WebAssembly, enhanced pattern‑matching syntax, and first‑class integration with Deno. Keeping an eye on the roadmap ensures you can plan migrations and adopt new capabilities without disruption.
Conclusion
ReScript delivers a compelling blend of speed, safety, and simplicity that makes it a strong contender as a JavaScript alternative. Its tiny runtime, fast compiler, and seamless interop let you adopt it gradually, while its strong type system catches bugs before they reach production. Whether you’re building a massive SPA, a serverless backend, or a data‑heavy dashboard, ReScript can reduce development friction and improve runtime performance. Give it a try on a small module today—once you experience the confidence of compile‑time guarantees, you’ll wonder how you ever lived without them.