DEV Community

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

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

Josh Derocher-Vlk on May 28, 2024

ReScript, the "Fast, Simple, Fully Typed JavaScript from the Future", has been around for awhile now. The name "ReScript" came into existence in 20...
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.

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
 
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
 
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
 
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.