ReasonML promised to bring OCaml's powerful type system to JavaScript development. Now, with Melange, that promise is more real than ever. Melange is a modern OCaml-to-JavaScript compiler that enables you to write React applications with the full power of OCaml's type system, pattern matching, and functional programming paradigms.
TypeScript is great, but it has fundamental limitations:
any, type assertions, and structural typing allow runtime errors to slip through.null and undefined, and they're not properly handled by the type system.OCaml (and by extension, ReasonML) offers:
option types, making null handling explicit and safe.Melange is a backend for the OCaml compiler that targets JavaScript. Unlike the older BuckleScript/ReScript, Melange:
reason-react1# Install opam (OCaml package manager)
2bash -c "sh <(curl -fsSL https://raw.githubusercontent.com/ocaml/opam/master/shell/install.sh)"
3
4# Initialize opam
5opam init -y
6eval $(opam env)
7
8# Create a new switch for your project
9opam switch create . 5.1.1 --deps-only -y
10eval $(opam env)
11
12# Install Melange and dependencies
13opam install melange reason-react reason-react-ppx dune
141my-react-app/
2├── dune-project
3├── dune
4├── package.json
5├── src/
6│ ├── dune
7│ ├── App.re
8│ └── Index.re
9└── public/
10 └── index.html
11dune-project#1(lang dune 3.8)
2(using melange 0.1)
3dune (root)#1(melange.emit
2 (target output)
3 (libraries reason-react)
4 (preprocess (pps reason-react-ppx))
5 (module_systems es6))
61/* Counter.re */
2[@react.component]
3let make = () => {
4 let (count, setCount) = React.useState(() => 0);
5
6 <div>
7 <h1> {React.string("Counter: " ++ string_of_int(count))} </h1>
8 <button onClick={_ => setCount(c => c + 1)}>
9 {React.string("Increment")}
10 </button>
11 <button onClick={_ => setCount(c => c - 1)}>
12 {React.string("Decrement")}
13 </button>
14 </div>;
15};
16One of ReasonML's killer features is variants (sum types). Here's a more sophisticated example:
1/* TodoApp.re */
2type todo = {
3 id: int,
4 text: string,
5 completed: bool,
6};
7
8type filter =
9 | All
10 | Active
11 | Completed;
12
13type state = {
14 todos: list(todo),
15 filter,
16 nextId: int,
17};
18
19type action =
20 | AddTodo(string)
21 | ToggleTodo(int)
22 | DeleteTodo(int)
23 | SetFilter(filter);
24
25let reducer = (state, action) =>
26 switch (action) {
27 | AddTodo(text) =>
28 let newTodo = {id: state.nextId, text, completed: false};
29 {
30 ...state,
31 todos: [newTodo, ...state.todos],
32 nextId: state.nextId + 1,
33 };
34 | ToggleTodo(id) =>
35 let todos =
36 state.todos
37 |> List.map(todo =>
38 todo.id == id ? {...todo, completed: !todo.completed} : todo
39 );
40 {...state, todos};
41 | DeleteTodo(id) =>
42 let todos = state.todos |> List.filter(todo => todo.id != id);
43 {...state, todos};
44 | SetFilter(filter) => {...state, filter}
45 };
46
47[@react.component]
48let make = () => {
49 let (state, dispatch) =
50 React.useReducer(
51 reducer,
52 {todos: [], filter: All, nextId: 0},
53 );
54
55 let filteredTodos =
56 switch (state.filter) {
57 | All => state.todos
58 | Active => state.todos |> List.filter(t => !t.completed)
59 | Completed => state.todos |> List.filter(t => t.completed)
60 };
61
62 <div>
63 <h1> {React.string("Todo App")} </h1>
64 <input
65 onKeyDown={e =>
66 if (ReactEvent.Keyboard.key(e) == "Enter") {
67 let value = ReactEvent.Form.target(e)##value;
68 dispatch(AddTodo(value));
69 ReactEvent.Form.target(e)##value #= "";
70 }
71 }
72 placeholder="What needs to be done?"
73 />
74 <div>
75 <button onClick={_ => dispatch(SetFilter(All))}>
76 {React.string("All")}
77 </button>
78 <button onClick={_ => dispatch(SetFilter(Active))}>
79 {React.string("Active")}
80 </button>
81 <button onClick={_ => dispatch(SetFilter(Completed))}>
82 {React.string("Completed")}
83 </button>
84 </div>
85 <ul>
86 {filteredTodos
87 |> List.map(todo =>
88 <li key={string_of_int(todo.id)}>
89 <input
90 type_="checkbox"
91 checked={todo.completed}
92 onChange={_ => dispatch(ToggleTodo(todo.id))}
93 />
94 <span> {React.string(todo.text)} </span>
95 <button onClick={_ => dispatch(DeleteTodo(todo.id))}>
96 {React.string("Delete")}
97 </button>
98 </li>
99 )
100 |> Array.of_list
101 |> React.array}
102 </ul>
103 </div>;
104};
1051type user = {
2 name: string,
3 email: option(string),
4};
5
6[@react.component]
7let make = (~user: user) => {
8 <div>
9 <h2> {React.string(user.name)} </h2>
10 {switch (user.email) {
11 | Some(email) => <p> {React.string("Email: " ++ email)} </p>
12 | None => <p> {React.string("No email provided")} </p>
13 }}
14 </div>;
15};
161type apiError =
2 | NetworkError
3 | ParseError(string)
4 | NotFound;
5
6type apiResult('a) = result('a, apiError);
7
8[@react.component]
9let make = () => {
10 let (data, setData) = React.useState(() => None);
11 let (error, setError) = React.useState(() => None);
12
13 React.useEffect0(() => {
14 fetchUser()
15 |> Promise.then_(result =>
16 switch (result) {
17 | Ok(user) =>
18 setData(_ => Some(user));
19 Promise.resolve();
20 | Error(err) =>
21 setError(_ => Some(err));
22 Promise.resolve();
23 }
24 )
25 |> ignore;
26 None;
27 });
28
29 <div>
30 {switch (error) {
31 | Some(NetworkError) => <p> {React.string("Network error")} </p>
32 | Some(ParseError(msg)) =>
33 <p> {React.string("Parse error: " ++ msg)} </p>
34 | Some(NotFound) => <p> {React.string("User not found")} </p>
35 | None => React.null
36 }}
37 {switch (data) {
38 | Some(user) => <UserProfile user />
39 | None => <p> {React.string("Loading...")} </p>
40 }}
41 </div>;
42};
43Melange makes it easy to use existing JavaScript libraries:
1/* Bindings for a JS library */
2[@mel.module "lodash"] external debounce: ('a => unit, int) => ('a => unit) = "debounce";
3
4[@react.component]
5let make = () => {
6 let (searchTerm, setSearchTerm) = React.useState(() => "");
7
8 let debouncedSearch =
9 React.useMemo1(
10 () => debounce(term => Js.log("Searching for: " ++ term), 300),
11 [||],
12 );
13
14 <input
15 value=searchTerm
16 onChange={e => {
17 let value = ReactEvent.Form.target(e)##value;
18 setSearchTerm(_ => value);
19 debouncedSearch(value);
20 }}
21 />;
22};
231# Build with Melange
2dune build
3
4# The output will be in _build/default/output/src/
5# You can serve it with any static server
6npx serve _build/default/output
7| Feature | TypeScript | ReasonML |
|---|---|---|
| Type Safety | Unsound (gradual typing) | Sound (no runtime type errors) |
| Null Handling | null and undefined | option type |
| Pattern Matching | Limited (switch) | Exhaustive, compiler-enforced |
| Immutability | Opt-in | Default |
| Learning Curve | Low (JS-like) | Medium (ML syntax) |
| Ecosystem | Massive | Growing |
Use it when:
Avoid it when:
ReasonML with Melange brings the rigor of OCaml to frontend development. While the ecosystem is smaller than TypeScript's, the type safety and expressiveness make it a compelling choice for teams building complex, reliable applications. If you've ever had a production bug caused by undefined is not a function, ReasonML might be worth the learning curve.
Technical Writer
NordVarg Team is a software engineer at NordVarg specializing in high-performance financial systems and type-safe programming.
Get weekly insights on building high-performance financial systems, latest industry trends, and expert tips delivered straight to your inbox.