DEV Community

Cover image for ReScript has come a long way, maybe it's time to switch from TypeScript?
Josh Derocher-Vlk
Josh Derocher-Vlk

Posted on

ReScript has come a long way, maybe it's time to switch from TypeScript?

ReScript, the "Fast, Simple, Fully Typed JavaScript from the Future", has been around for awhile now. The name "ReScript" came into existence in 2020, but the project has a history going back to 2016 under the combo of Reason and BuckleScript. The name change came about as the goal of BuckleScript shifted to try and create a language that was a part of the JavaScript ecosystem; JavaScript is the only build target, support for all of the features JavaScript devs expect to have like async/await syntax, and an easy to use standard library for JavaScript's built in functions.

Since the re-brand to ReScript the team behind the project have been very busy. If you look around the internet to see what people have to say about ReScript there are some things that people didn't like or JavaScript features that were missing. Many of these have been addressed, so lets take a look at some of these complaints and features that have been recently addressed. Hopefully you'll give ReScript another look as a strong alternative to TypeScript

My sources of past issues

To create this list of issues I have poured over the official ReScript forum, Hacker News, Reddit, and Twitter. I tried not to omit anything and I wasn't just looking for issues that have been solved. I'll call out anything that hasn't changed that might still be a pain point for some people, but hopefully you'll see the massive steps forward the language has taken in the past 4 years.

I have to understand Reason, BuckleScript, and OCaml before I can use ReScript

You don't need to know anything about Reason, BuckleScript or OCaml to use ReScript.

ReScript's history comes from these languages and tools, but today it's very much it's own thing that works with the standard JS ecosystem of NPM, PNPM, Yarn, or whatever package manager you want to use. The JS it spits out can be used by any build tool like Vite, ESBuild, Webpack, etc... ReScript is fully part of the JS ecosystem and you don't need to know any other languages, new build tools, or new package managers.

Ocaml is still a wonderful language if you want to look into it, and Reason is still going strong as an alternate syntax for OCaml. With either OCaml or Reason you can compile to native code, or use the continuation of BuckleScript now called Melange.

Should I use Belt.Array, Js.Array, or Js.Array2?

Due to ReScript's history being rooted in Ocaml and Reason, it came with some baggage from those languages. Not bad baggage, but Ocaml isn't JavaScript and had it's own patterns and ways of doing things that made sense for that language. Trying to match these patterns to JavaScript was cumbersome when the goal of Reason was to compile to native code or JavaScript while maintaining full interop with OCaml. ReScript now focuses only on the land of JavaScript, so it has a new Core library that is meant to be easy to pick up and understand. The previous standard Js library is being deprecated and will soon be removed from the language, and Belt will evolve into a more fully featured utility library like Lodash that will be optional to use.

The answer to the above question is to just use Array.



let t = [1, 2, 3]->Array.map(n => n + 1)


Enter fullscreen mode Exit fullscreen mode

Core is a separate library for now, but the roadmap is to include it in the language directly before the end of the year.

Lack of async, await, and Promises are cumbersome

Native support for async/await syntax was added in ReScript 10.1 in Early 2023. The new Core library I mentioned above also introduced a streamlined way to work with Promises.



// async/await
let fn = async () => {
  try {
    let result = await getData()
    Console.log(result)
  } catch {
  | err => Console.error(err)
  }
}

// Promises
let fn = () => {
  getData()
  ->Promise.thenResolve(result => Console.log(result))
  ->Promise.catch(err => {
    Console.error(err)
    Promise.resolve()
  })
}


Enter fullscreen mode Exit fullscreen mode

Lack of community types

When ReScript was young you would have to write your own bindings and types for most existing JS libraries before you could use them. There are now hundreds of NPM packages that have bindings and documentation on how to use your favorite JS libraries with ReScript. Bindings exist for React (which also still has first class support directly in the ReScript compiler), Node, Jotai, React Query, React Hook Form, AntD, Material UI, Bun, Vitest, Graphqljs and many others!

There are even some incredible ReScript first libraries out there that really shine and can take advantage of ReScript powerful type system and compiler, like rescript-relay.



/* Avatar.res */
module UserFragment = %relay(`
  fragment Avatar_user on User {
    firstName
    lastName
    avatarUrl
  }
`)

@react.component
let make = (~user) => {
  // this is fully typed!
  let userData = UserFragment.use(user)

  <img
    className="avatar"
    src=userData.avatarUrl
    alt={
      userData.firstName ++ " "
      userData.lastName
    }
  />
}


Enter fullscreen mode Exit fullscreen mode

ReScript also has amazing tooling for parsing JSON into types. Here's an example of rescript-schema:



@schema
type cat = {
name: string,
color: string,
}

let _ =
%raw({ name: 'fluffy', color: 'orange' })
->S.parseWith(catSchema) // catSchema is generated automatically!
->Console.log

Enter fullscreen mode Exit fullscreen mode




I don't understand how to use existing JavaScript libraries

While there is a growing number of community bindings and libraries written in ReScript, at some point you'll need to dig into writing your own bindings. Thankfully, the documentation is excellent and you can reference one of the many existing NPM packages for ideas.

Just this week I needed to use a function from path-to-regexp and it took me just a few minutes to create bindings and the function I needed.



type t = string => option<{.}>

type m = {
path: string,
params: {.}, // this means any object
}

@unboxed // @unboxed is a way to have a variant type that compiles to simple javascript without a runtime object
type isMatch =
| Yes(option<m>) // the JS for this is just an object of type m
| @as(false) No // the JS for this is just 'false'

type match = string => isMatch

@module("path-to-regexp") // this is the actual binding using the types I made above
external match: string => match = "match"

let make = url => {
let fn = match(url) // and now I can call it!
path =>
switch fn(path) {
| Yes(t) => t->Option.map(t => t.params)
| No => None
}
}

Enter fullscreen mode Exit fullscreen mode




I can't have 2 files with the same name?

This hasn't changed, and due to the way the module system works it never will. It might feel weird at first for JS devs, but this isn't that weird in other languages. You get used to it very quickly.

Why would I pick this over TypeScript?

I have another article that dives into this: Tired of Typescript? Check out ReScript!

The short answer is that you would pick ReScript if:

  • You want a strong, sound type system (no any types!)
  • You want types, but you don't want to write type annotations (the compiler just knows!)
  • You want only the "Good Parts" of JavaScript
  • You want a blazing fast compiler and fast Intellisense in VSCode, even when your repo is massive
  • You want amazing next gen language features like pattern matching, Option and Result types, and powerful variant types.

Not convinced?

Leave a comment! I'm more than happy to discuss things in more detail and answer questions about ReScript! I've been using it for a few projects over the past 4 years, and it's hard to switch back to writing TypeScript when I need to. It's been incredible to watch the language grow and evolve and it should be considered as an alternative to TypeScript.

Cover Photo by Patrick Tomasso on Unsplash

Top comments (49)

Collapse
 
hakimio profile image
Tomas Rimkus

Regarding your "Why would I pick this over TypeScript?" points:

  • You can have TS code without "any". Just set-up ESLint rule to disallow "any".
  • Type inference is really good now in TS. TSC can auto-detect types in most of the cases where it makes sense. Typing a and b variables as numbers by default when code is let add = (a, b) => a + b doesn't make sense to me. Calling this add() function with strings is perfectly valid in JS.
  • If you want only "the good parts", then only use them. Use a linter to disallow the "bad" code constructs.
  • TSC is pretty fast nowadays.
  • Why would I want to have Rust code in my JS/TS code? I want better JS, not "Rust in JS".

Don't really see why any TS developer would want to switch to this.

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk

First off, thanks for taking the time to read and leave a comment! I'll answer your points one by one.

You can have TS code without "any". Just set-up ESLint rule to disallow "any".

You need a secondary tool configured correctly to handle this, and developers can easily by-pass that with a quick ESlint ignore comment. ReScript does not require additional tooling to enforce type safety, and it's not dependent on how you have it configured. Everyone works with the same strong type system.

Type inference is really good now in TS. TSC can auto-detect types in most of the cases where it makes sense. Typing a and b variables as numbers by default when code is let add = (a, b) => a + b doesn't make sense to me. Calling this add() function with strings is perfectly valid in JS.

That is valid code in JS, which of course makes it valid code in TS. In ReScript the + operator is only for adding together int. There's +. for float and ++ for string. Because the type system is simple everything can only ever be of one type, so function calls and operators can only ever work with one type. This allows it to always correctly know the types based on usage.

While you can rely on type inference, it can really slow down the compiler in large projects. Microsoft recommends that you use type annotations to avoid slow downs on large projects. The ReScript compiler will not slow down on large codebases or when you omit annotations.

If you want only "the good parts", then only use them. Use a linter to disallow the "bad" code constructs.

This again depends on an external tool to enforce, which can be configured in any number of ways. You can also rely on code reviews and establish patterns on your team for what you consider to be the "bad" parts. ReScript doesn't have all of the JS baggage, which means you don't need to work around the footguns of JS or add extra linters.

TSC is pretty fast nowadays.

Yes, it is getting much better, but it's still not "blazing fast". I have project with 32k TypeScript files that takes 2 minutes to run full typechecking with TSC on a cold start, and saving a file with watch mode on takes 4 seconds to type check. A ReScript project with 50k files takes 1 minute to fully compile on a cold start, and when saving it takes under 400 ms to typecheck. It's enough of a difference to greatly improve the feedback loop of local development. I've never once had to wait to see type definitions in VSCode when using ReScript.

Why would I want to have Rust code in my JS/TS code? I want better JS, not "Rust in JS".

Rust is one of the most loved languages by developers, and for good reason. Working with tagged unions, pattern matching, and option and result types make it easier to handle business logic and prevent bugs. I dig more into that here: dev.to/jderochervlk/rescript-rust-...

Don't really see why any TS developer would want to switch to this.

There are probably a ton of TS devs that are happy with the state of TS, but there are also many devs forced to work with TS coming from other languages that want something better, or maybe they just want a language that works out of the box the same way for everyone using it without the need for external linters and formatters. TS will probably be on top for a long time, but it's nice to have other options available.

Collapse
 
dagnelies profile image
Arnaud Dagnelies • Edited

I have project with 32k TypeScript files ...

A ReScript project with 50k files ...

OMG! Split that in smaller modules/packages dude! 🤣 ...and then you won't ever have a problem ever again with the typechecking performance as bonus.

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

Haha we have since broken that up into a monorepo, for better TS and eslint performance. While monorepos can be a nice solution, I don't like that we were forced into it to avoid VSCode crashing on us.

Thread Thread
 
dagnelies profile image
Arnaud Dagnelies

I still can't wrap my head around it. A project with 50k files ...just navigating it must be a nightmare ...and the resulting bundle must be so heavy. What the heck does your software do to be that large?! Just for context, out of curiosity, I checked how many source files the react repo has. It's a monorepo containing 26 packages, totalling ~1600 files. That's more human. You definitely don't have some average project.

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

It's a project with 40 devs working in one repo. A good chunk of it is probably not even used, but we keep adding to it. This also includes unit tests.

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

I've worked on larger projects at saas companies in the past, things can get complicated and large real quick.

Thread Thread
 
dagnelies profile image
Arnaud Dagnelies

Man... split that monster ...it's overdue ...40 people on the same repo is too much ...I don't get it

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

Hey, Google has just 1 repo, so if it works it works.

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

But seriously, we have a large website that requires a lot of devs to build features at the pace we want, and the teams often work on the same pages. We've tossed around the idea of a poly repo approach, but we'd have to pull in federated microfrontends, which are a headache, and even then devs would still just be locally linking 4 repos together to complete a feature. A monorepo was a good choice for us.

Thread Thread
 
dagnelies profile image
Arnaud Dagnelies

So what's the website requiring this 50k files? Now I'm even more curious of that beast :P

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

You would be very disappointed lol. Its a lot of logic for a/b testing and landing pages that most people will never see.

 
dagnelies profile image
Arnaud Dagnelies

Isn't Google's homegrown version-control system a piece of history by now? ...somehow I doubt they have a monorepo in the sense we speak of. I also think they switched to git by now.

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

They do have a massive amount of tooling, and as far as I know they still have billions of sloc in a single repo. research.google/pubs/why-google-st...

Microsoft has a million files in 1 repo: devblogs.microsoft.com/bharry/the-...

Collapse
 
hakimio profile image
Tomas Rimkus • Edited

Thank you for taking the time to write such an extensive answer 🙂
After reading your "ReScript: Rust like features for JavaScript" article, I have to admit that ReScript does have some very nice features and should be a great choice for people looking for a better than JS, simple and fast scripting language.
BTW, talking about simple and fast languages, do you know "V programming language" (vlang.io)? It's basically a better version of Go. Used V for a hobby project and loved its simplicity.

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

I'll have to check it out, thanks!

Collapse
 
dagnelies profile image
Arnaud Dagnelies • Edited

I think the great strength of typescript is that it's not trying to replace javascript, but rather to play along with it. That's the reason of existence for types like "any" and many other design choices.

There were countless attempts of languages "compiling into" JS. They all failed, since decades. The reason is that the important thing is not the language. It's the ecosystem. The sheer amount of libraries, the frameworks, the tooling, etc. Sure, you can use your new fancy language with a super syntax in your bubble ...but it'll stay there, in the small bubble.

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

I agree 100% that easy interop and gradual adoption is what led to TypeScript's success.

Other compile to JS languages have always been slightly outside the JS ecosystem, such as Elm, PureScript, Reason, or Clojure. They had they're own package manager, frameworks, or difficult interop with JS. ReScript is on NPM and you can easily use it with your existing build tools, or even partially use it in a JS/TS project. It's not a separate ecosystem, which is what hurt adoption of other compile to JS languages.

Thread Thread
 
dagnelies profile image
Arnaud Dagnelies

But I can't use it in my IntelliJ or the integration may be crappy, I can't use it in vue or react or angular. Heck, I can't even use another lib if it hasn't an "any" type! ...and please don't tell me I should just write the definitions of every lib I want to use first, that would be so contra-productive.

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

The VScode integration is great actually and has had a ton of effort put into it.

And for any JS library you want to use, you'll either need to write some bindings for the functions you use (which is usually not everything in a library) or find one published on NPM. It's similar to typescript in 2017ish, where you had to find types on NPM or write a d.ts file.

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

Rescript has JSX and React support out of the box, which covers a lot of ground for most web dev projects.

Thread Thread
 
dagnelies profile image
Arnaud Dagnelies

Still, I guess you can notice your arguments are not really convincing.

While I respect your efforts and it is certainly impressive, I sadly cannot encourage your endeavor. It just leads to even more fragmentation of the community, which I consider counter-productive from a birds eye perspective.

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

I can't say I come to the same conclusion, but I always expect to have typescript defenders pop up. I had this same debate about why typescript was good in the first place, and again when everyone hated JSX. Progress is tough, and adoption is hard. Not everything will break through, but I try and advocate for the tools that I believe will make our lives as developers easier.

Thread Thread
 
dagnelies profile image
Arnaud Dagnelies

Sure, and you are right to so. It's also not that I love TS or JS ...actually, I find them kind of poor. JS is kind of fucked up if you think about it. There is no sensible language where [77, 5, 123].sort() would lead to [123, 5, 77]. But hey, we have to live with, that's where all the millions of libs, and the frameworks, and the tooling is.

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

Maybe someday WASM will save us all.

Collapse
 
vasco3 profile image
JC

support counter arguments!

Collapse
 
vasco3 profile image
JC

good points but you are really missing out in the powerful pattern marching variants with the switch

Collapse
 
tombohub profile image
tombohub

I wish I can downvote because of last sentence. so this is downvote

Collapse
 
spocke78 profile image
Johan Sörlin • Edited

Good post. I done bigger projects in both rescript and typescript and I must say rescript is really a wonderful language. Having a properly inferred sound nominal type system is one of the biggest things. Just because a thing looks like a duck, quacks like a duck it doesn't mean its a duck. Having the ability to alias a string to a custom type then hide it using an interface file is super powerful.
Other things like having tail recursive functions be compiled down to a while loop is something TS can't do.
Having named arguments for functions reduces the need for adding objects all over the place.
Having proper sum types not the discriminated unions in TS that are so verbose that they are pretty useless.
Having a blazingly fast compiler really changes how you develop things.
The list just goes on and on.
Typescript has some fancy type magic as well that rescript doesn't but really never missed that when doing rescript. The type system is just cleaner, simpler and more elegant.
I think a lot of TS developers really need to try this language write a 10kloc project in it then you can say what you think about it. I think a lot of people would be converted if they just past the barrier and are open minded.

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk

Once you have sum types and pattern matching you wonder how anyone gets anything done without them.

Collapse
 
spocke78 profile image
Johan Sörlin

Yes once you have proper sum types you start to reason about problems that way and it frustrating when you don't have that language feature.

Collapse
 
joefiorini profile image
Joe Fiorini

Thanks for this post! It's been a few years since I've heard about ReScript, I'm very glad to hear it's still going strong!

For anyone intrigued by this language remember that it does compile to very readable JavaScript. IIRC it would be human editable if need be. That means you have a very easy gradual migration path (and a fallback if you don't end up liking it). No reason not to try it out.

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk

Yup! It's very easy to just commit the compiled JS to GitHub and delete rescript. You would probably want to clean up some function names, but it'll work as is and be readable.

Collapse
 
srijanbaniyal profile image
Srijan Baniyal

Well this is something new . I guess trying is not too harmful ..

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk
Collapse
 
jangelodev profile image
João Angelo

Hi Josh Derocher-Vlk,
Top, very nice !
Thanks for sharing

Collapse
 
nigel447 profile image
nigel447

thanks for taking the time to write this, I am always looking for ways to avoid Typescript and this looks promising, my initial reaction to the synatx below is negative

->Promise
Enter fullscreen mode Exit fullscreen mode

only as it reminds me of PHP in the scheme of things but that is a non issue

does it have "Either" ?
question arises w.r.t your section on bindings the final actual pattern matching seems to me a bit verbose, I am not an OCaml dev but could that not be a simple Either?

Rescript looks really good

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk • Edited
->Promise
Enter fullscreen mode Exit fullscreen mode

This is the pipe syntax. If you haven't worked with a language that has it, it can be really strange to see, but once you get used to working with pipes you never want to work in a language that lacks them.

Without pipes that last function would have looked like this:

let fn = () => {
 let data  = getData()
 let result = Promise.thenResolve(data, result => Console.log(result))
 Promise.catch(result, err => {
    Console.error(err)
    Promise.resolve()
  })
}
Enter fullscreen mode Exit fullscreen mode

does it have "Either" ?

It has a Result type.

the final actual pattern matching seems to me a bit verbose

Yeah, I selected my most recent bindings and the underlying library has a weird API, so it was tricky to keep simple. It returns an object, 'undefined' or false. I had to make a custom variant type to figure that out. Variant types can have any name, and choosing Match and False is probably not clear. This is internal to the module I was working on, so the outside doesn't have to look at it.

It would be clearer to have named it Yes and No like this:

type t = string => option<{.}>

  type pathMatch = {
    path: string,
    params: {.},
  }

  @unboxed
  type isMatch =
    | Yes(option<pathMatch>)
    | @as(false) No

  type match = string => isMatch

  @module("path-to-regexp")
  external match: string => match = "match"

  let make = url => {
    let fn = match(url)
    path =>
      switch fn(path) {
      | Yes(t) => t->Option.map(t => t.params)
      | No => None
      }
  }
Enter fullscreen mode Exit fullscreen mode

I've updated the example in the article (and in my project!) to be a bit more clear.

Collapse
 
nigel447 profile image
nigel447

amazing

Collapse
 
nigel447 profile image
nigel447

yes definitly better than Either in this case

Collapse
 
sirajulm profile image
Sirajul Muneer

At the end it doesn’t matter. These languages doesn’t provide runtime type safety just because they are transpiled to javascript which doesn’t have type safety. So in essence these “type safety” languages just benefit on IDE’s

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk

"Runtime type safety" isn't really a thing, even Haskell and Rust are compile-time checks.

ReScript is a compiled language, so it's more than just a check for the IDE. If I have a source file that has a type error in it, that file will never become JavaScript, which means it will never run on Node or in the browser. Unfortunately for TypeScript, this usually isn't the case since most build tools are just stripping out the type information and we type check as a separate step in our build pipeline.

Collapse
 
sirajulm profile image
Sirajul Muneer

Rust does go way beyond simple type checking. The robust static type checking and ownership model is strong enough to ensure the application is stable even during runtime.
Sugar-coated languages doesn't bring any benefits of any of their typing models once they are compiled to Javascript which is purely untyped language.
If you are comparing the rust binaries to some javascript bundles then you are comparing apple to oranges.

Yes, if you are looking for auto-completes, documentation and development experience, yes these fancy languages are good on IDE's.

I can technically write a function that receive input as int for example and still bind it to an html number input that returns a floating point. Which completely defeats the purpose.

Collapse
 
psb profile image
Paul Bacchus

One downside of not being able to have two files with the same name is that when you use a React framework which uses file based routing you have to wrap ReScript React code/components in a JS or TS file, because you can't have multiple page.res or route.res files.

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk

Yeah, that's the one pain point I've found, but the workaround is pretty easy. You have to create thin JS files with the correct name and folder that just re-export from the compiled ReScript files.

Collapse
 
adicandra1 profile image
adicandra1

I don't really like the weird "~" and "->" as opposed to plain "."
when you want to appeal average typescript devs, you need to make it seamless familiar to them.
Typescript is already good enough most of the case

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk

The ~ and -> are not equivalent or replacements for ..

~ is a labled argument which allows you to pass in an argument in any order, it compiles down to an object in JS.

let greet = (~name) => `Hello ${name}`

Console.log(greet(~name="Josh"))
Enter fullscreen mode Exit fullscreen mode

rescript-lang.org/docs/manual/late...

And -> is the pipe operator. It takes the previous value and passes it as the first argument of the next function.

"Hello"->Console.log
// is the same as
Console.log("Hello")
Enter fullscreen mode Exit fullscreen mode

rescript-lang.org/docs/manual/late...

Collapse
 
jakewilson profile image
Jake Wilson

You lost me at

you'll need to dig into writing your own bindings

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk

We had to do this with TypeScript for years.