DEV Community

Cover image for Tired of Typescript? Check out ReScript!
Josh Derocher-Vlk
Josh Derocher-Vlk

Posted on

Tired of Typescript? Check out ReScript!

If you've spent any amount of time developing with JavaScript you have probably found it difficult to maintain a large code base and avoid runtime errors. The language wasn't designed to do all of the things we use it for today. We've been trying to improve our experience with the language by using tools like ESLint to improve and enforce a shared code quality with our teams. One of the largest shifts in the JavaScript world has been the adoption of TypeScript, which adds a nice layer of types on top of JavaScript. It has done a fantastic job assisting with code maintainability in large JavaScript projects.

However, TypeScript is not without its flaws. It can be slow to compile and show type hints in your IDE, the types require a lot of annotation and can create too much noise around simple code, the type system has unsafe escape hatches like any that reduce our level of trust in the types, and since it's a superset of JavaScript it brings along all of JavaScript's warts and footguns. These issues have led some people to drop TypeScript in favor of using JSDoc with types or even dropping types all together.

Instead of moving back to un-typed JavaScript, perhaps there is another solution?

ReScript

ReScript is a fully typed language with an easy to understand JS like syntax, blazing fast compiler, that compiles to JavaScript. You can easily drop it into an existing project, and there is even a way to generate TypeScript types if you want to add it to a TypeScript project!

While ReScript and TypeScript fill a similar role of improving JavaScript development with types they have different goals and ways of achieving those goals. TypeScript is a superset of JS, which means all valid JS is valid TS and that TypeScript's type system has to be able to add types to anything JavaScript can do. ReScript is a different language with a JS like syntax, so it doesn't have to try and add types to JavaScript code. It's built for types.

Less type annotations

TypeScript requires you to add type annotations to your code in order for the compiler to understand what you are trying to do.

function add(a: number, b: number) {
  return a + b
}
Enter fullscreen mode Exit fullscreen mode

Thankfully it can usually infer the return type. If I omitted the types here I would get an error:

function add(a, b) /* Parameter 'a' implicitly has an 'any' type.ts(7006) */ {
  return a + b
}
Enter fullscreen mode Exit fullscreen mode

ReScript can infer the types based on how you use the values, and when I say infer I don't mean "guess", the type is guaranteed to be correct.

let add = (a, b) => a + b // fully typed!
Enter fullscreen mode Exit fullscreen mode

Here's a slightly larger example:

type person = {name: string}

let greet = person => `Hello ${person.name}`

let bill = {
  name: "bill",
}

let main = () => greet(bill)
Enter fullscreen mode Exit fullscreen mode

That is all fully typed. ReScript knows that greet takes in a person since it accesses name. It knows that bill is a person because it's a record with a name field. So I can call greet with bill. You can take a look at this code with type hints on hover on the ReScript playground.

Here's how we can do the same in TypeScript:

type Person = { name: string }

const greet = (person: Person) => `Hello ${person.name}`

const bill = {
  name: 'bill'
}

const main = () => greet(bill)
Enter fullscreen mode Exit fullscreen mode

It's not much more in this example, but it starts to add up quickly as code gets more complex.

Another cool example is that this also works for React component props (ReScript has first class support for React and JSX!).

@jsx.component
let make = (~name) => <p> {React.string(`Hello ${name}!`)} </p>
Enter fullscreen mode Exit fullscreen mode

It knows that name is a string! Here's the equivalent in TypeScript:

type Props = { name: string }

const Greeting = ({ name }: Props) => `Hello ${name}`
Enter fullscreen mode Exit fullscreen mode

My favorite part of this it not having to remember the correct type for every event handler in React.

@jsx.component
let make = (~handleClick) =>
  <button onClick=handleClick> {React.string("Click me!")} </button>
Enter fullscreen mode Exit fullscreen mode

Removing the need for explicit type definitions allows me to move quicker, and it allows me to add new props easily. You don't have to think about types unless you are defining a shape of data. The types are there acting as a strong guiding hand, but you don't need to tell ReScript what type is which, it just does that for you. Of course, you can always add type annotations if you would like.

let add = (a: int, b: int): int => a + b
Enter fullscreen mode Exit fullscreen mode

Better type system

ReScript has a "sound" type system. In a nutshell this means that if the compiler assigns type a to something we know it has to be type a. I found this conversation around the topic helpful.

How does that help us as developers?

If we see a function that returns a string can know without a doubt that it will return a string.

Doesn't TypeScript do this? It can as long as everyone plays the game correctly, but it's very easy to trick or lie to TypeScript, or to just incorrectly assign something that has an any type.

const t1 = 42 as unknown as string

// @ts-ignore
const t2: string = 42

const fn = (): any => 42

const t3: string = fn()

const t4: any = 42

const t5 = t4 as string
Enter fullscreen mode Exit fullscreen mode

If you saw some of these pop up in a code review you would hopefully demand changes, but it's easy in real world code to let this type of error bleed out into the wild.

type person = {
  name: string,
  age: number
}

function parse(t: string): person {
  return JSON.parse(t)
}

const t1 = parse('{"color": "blue"}')
Enter fullscreen mode Exit fullscreen mode

I have seen something like this in real code, and it led to bugs. ReScript won't allow you to do this, you will have to parse the JSON with a library like rescript-schema (it's like Zod).

ReScript doesn't have an any type. Everything has to have some sort of type, and I can't tell the compiler to ignore the next line or force it to assign a type to a value that is incorrect.

Having a type system you can trust helps you write code with confidence.

Faster compile times

TypeScript can slow down if you have a large enough code base. I've worked on projects with upwards of 150k lines of TypeScript and VSCode would crash frequently and often fail to show type hints on hover.

Your mileage may vary, but here are some numbers from projects I have been working on recently. In a project with almost 32k TypeScript files it takes me 2 minutes to run full type checking. When running tsc in watch mode it takes 4-10 seconds to type check after saving a file.

A ReScript app with 50k files can run full type checking with no cache in 1 minute. After a cache is made this takes no time if nothing has changed, or milliseconds if you have changed things. With all 50k files being watched it takes 338 milliseconds for ReScript to type check and compile after saving a file.

Even with 50k files VSCode intellisense picks up on every module with auto complete without any lag.

When using ReScript, you also don't need tools like ESLint or Prettier. The compiler enforces good practices and it has a built in formatter.

Working on a large project with snappy intellisense and a quick feedback loop is amazing. ReScript doesn't slow down local development when your project grows, and even full builds scale well and can still run full type-checking in half the time it takes TypeScript.

The good parts of JS and more!

ReScript has all of the good parts of JS that we like using such as await/async, object and array spreading, destructuring, and it also has new features like pattern matching, variant types, and Option and Result types that should feel familiar to developers familiar with Rust, Elm, or OCaml.

You can read more about these features in my other posts:

Top comments (15)

Collapse
 
efpage profile image
Eckehard

Sounds great. How is the VScode-integration of ReScript? Can wen enable syntax checks while typing?

Collapse
 
dzakh profile image
Dmitry Zakharov

There's an incremental type-checking recently added in the VSCode extension. You can enable it by adding this in your VSCode settings:

"rescript.settings.incrementalTypechecking.acrossFiles": true,
"rescript.settings.incrementalTypechecking.enabled": true,
Enter fullscreen mode Exit fullscreen mode
Collapse
 
zth profile image
Gabriel Nordeborn

Great article, thank you!

Collapse
 
efpage profile image
Eckehard

How would ReScript deal with pure Javascript code? Assume you have a large number of libraries. Ideally I would like to incrementally add type annotations to the code, while untouched code is still running.

Is this expectation too idealistic?

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk

ReScript has a way to bind to any JS library or file: rescript-lang.org/docs/manual/late...

It's pretty easy to do once you grok the concept, and it's much simpler than JS interop in other languages like Elm or F#.

You would convert over one file at a time, or even split out functions or components into a new file. I've migrated over a couple projects and I've found it easy to start with the smaller parts of an app, like hooks and functions, and the work up to components. The ReScript code can have bindings to the JS code, and the JS code can import from the compiled ReScript.

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk

I did a walkthrough here if you want to see an example: dev.to/jderochervlk/converting-a-j...

Collapse
 
efpage profile image
Eckehard

But how much changes would a library need to migrate from JS to ReS? As far as I understood native code should be widely compatible with ReScript?

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

It depends, but unlike TypeScript it's more of a process than renaming the file and seeing it compile. I tend to make rescript file to start moving over a component or functions into one by one and importing those functions back into JS land as I go.

My steps are usually:

  1. remove imports since rescript doesn't have imports, everything has to be in a rescipt code or have bindings somewhere. Depending on how many external libraries you use this might be easy or take some time, but once you have bindings for the core dependencies it goes quicker. Of course major libs like React already have good bindings for rescript you can find on npm
  2. fix all the declarations to use just let binding. Rescript doesn't have var, const, or function declarations. You can usually do this with a search and replace.
  3. Remove any returns. Rescript returns whatever the last expressions is, so you don't have/need a return keyword
  4. Make sure you have types for any objects you might be using. This again might take longer for the first time in a repo, but usually once you have the key ones defined it becomes easier

I've done a half dozen migrations from JS/TS to ReScript and it usually takes me a day of work, one took me 3 days because it was really 2 websites with a heavy dependency on external libraries that were a pain to create types and bindings for.
I've always been really satisfied once I have a project running on Rescript.

I've also converted many thousands of lines of non-strict typescript to strict typescript, and I can say that the migration is much easier to move to ReScript.

Thread Thread
 
efpage profile image
Eckehard

Thank you for your valuable feedback. It seems, it´s more than just adding some type declarations. Some of the JS concepts (specially the ES6-module system) are really quirky, so it is probably a good decision to go in a different direction.

I´m just not too happy about what might happen in the future. JS will keep on moving, so it is an important question if a language like ReScript (and even typescript) can keep up with this development. It's easy to get onto a dead track if JS does some bigger steps.

Thread Thread
 
jderochervlk profile image
Josh Derocher-Vlk

The majority of what I've done with ReScript has been ESM compatible with Vite and node 20. The good thing about working in a language that compiles to JS is that if something in JS changes, it doesn't mean your rescript code has to change. The compiler can always be updated to output different JS if needed.

One of the things that makes JS a terrible language to work with, but a great runtime target, is that nothing ever gets removed or deprecated from the language. So if your JS or Rescript works today, it will work in the future.

Collapse
 
fhammerschmidt profile image
Florian Hammerschmidt

Only twice as fast as TypeScript? Does not sound right to me, but it of course extremely depends on what dependencies you have installed because some really tend to bring the (turing-complete) type system of TS to its limits.

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk • Edited

This is with 32k TypeScript vs 50k rescript, so it's more than twice as fast. It's not that complicated of a typescript project, just react and some basic libraries. I have a small typescript project with a couple thousand files that takes almost 5 minutes to type check because it has a lot of overloaded function declarations and generic types.

You can optimize typescript to be fast by doing things like making sure functions have return types, but it's something you have to really work at. ReScript is just fast out of the box.

Full builds taking half the time TypeScript does is great, but the biggest win to me is gradual type checking while I work being almost instant. I can open up a file in VSCode and see type hints the moment I hover around the code. I save and get feedback about a broken type in another file in under a second.

Collapse
 
insign profile image
Hélio oliveira

Dart exports to js

Collapse
 
coder_one profile image
Martin Emsky

Is it currently possible to use data-attributes inside JSX in Rescript?

Collapse
 
jderochervlk profile image
Josh Derocher-Vlk • Edited

Yes. There are a few already available in rescript-react like <div dataTestId="foo" /> which compiles out to <div data-testid="foo"/>.

If you want something custom you do have to define a custom JSX transform with the data-atrributes you want to have with the types they accept. You can do this per file, or ideally you'll want to do it once for an entire project.

Here's a forum post with some details on how to set it up: forum.rescript-lang.org/t/custom-d...