Tired of Typescript? Check out ReScript!

RMAG news

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
}

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
}

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!

Here’s a slightly larger example:

type person = {name: string}

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

let bill = {
name: “bill”,
}

let main = () => greet(bill)

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)

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>

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

type Props = { name: string }

const Greeting = ({ name }: Props) => `Hello ${name}`

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>

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

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

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”})

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:

ReScript: Rust like features for JavaScript
Using variant types in ReScript to represent business logic

Leave a Reply

Your email address will not be published. Required fields are marked *