Your Web News in One Place

Help Webnuz

Referal links:

Sign up for GreenGeeks web hosting
January 20, 2021 07:59 pm GMT

How I Switched from TypeScript to ReScript

A glimpse into a more civilized (yet challenging) tool in the JavaScript ecosystem

Article originally published at Medium

This is not evangelism of ReScript or a one-to-one comparison with TypeScript. I love TypeScript. I decided to rewrite a small TypeScript+React+Jest side project into ReScript.

ReScript is not new. In a way its as old as JavaScript itself. ReScript is a rebranding of ReasonML (Facebook) and BuckleScript (Bloomberg), which wrap OCaml on both ends. The former is an interface of the OCaml syntax, while the latter makes sure to compile the AST into JavaScript. ReasonML was created by Jordan Walke, the creator of React. ReasonML still exists as a parallel project to ReScript, with a slightly different syntax and mission.

Image for post

ReScript syntax compiling into OCaml Abstract-Syntax-Tree, and BuckleScript compiling into readable, optimized JavaScript

ReScript is not just a rebranding: its a ReasonML which freed itself of the yoke of the OCaml ecosystem. By doing so, it forfeited compilation to native code and OCaml library interop, but gained a freer syntax which further resembles JavaScript to embrace its developers, eager for better tools.

First Impression

My first attempt was to just install ReScript on my project, start the watcher, rename an easy file into .res and be guided by the errors. I immediately learned that refactoring into ReScript is not breadth-first but depth-first. Simply renaming the file extension wont work, as the compiler stops completely at type errors.

In TypeScript one can gradually assign types and interfaces to dynamic types, while tagging some as unknown or any. Depth-first means that you start with one small function, or one small React component, and write it properly. If all the types are right and with mathematical precision your code will compile into JavaScript.

While TypeScript often transpiles into unreadable code, its good practice to keep an open tab on the auto-generated js file from ReScript. Youll be pleasantly surprised by the speed of transpilation, the conciseness and readability of the code, and the performance of such code. If the ReScript code compiled, it means its types are safe and sound, so it can optimize away all the noise.

The only exception I saw to readability and performance of the generated JavaScript was in curried functions. All functions in ReScript are curried by default, and some of them generate code which imports a Currying library. This didnt happen often, and currying can be disabled.

But what about TypeScript? Inter-operation with JavaScript code is trivial, but importing and exporting types from TypeScript (or Flow) can be more complex, and it creates two sources of truth: one for ReScript types and another for TypeScript.

GenType, described below, auto-generates a typed tsx file from your ReScript code which you can import into other modules. This helped for exporting ReScript types, but its not possible to import TypeScript ones. The automation of type conversions eased the problem of the two sources of truth.

Furthermore, the generated ts code uses CommonJs require syntax, which break when using native ECMAScript module support. I also had to tweak my tsc to not transpile the auto-generated tsx into a fourth (!) source file:

  • .res ReScript source code.
  • .bs.js compiled JavaScript, which you can ignore in your source control
  • .gen.tsx auto-generated by GenType, which import the compiled JavaScript code and re-export it with proper types. Also add to your .gitignore.
  • .gen.jsx accidentally transpiled by TypeScript, delete it and reconfigure your tsconfig.json.

I first rewrote my algorithms, since they didnt have any third-party imports to inter-operate with, and the import syntax was daunting for me at first. Some teams go for a data-first strategy, or a UI-first one (as Facebook did in 2017 for Messenger.com, rewriting 50% of the codebase).

Types

ReScript is part of the statically typed functional programming language family, which means its not compiling. Just kidding, it means it uses the Hindley-Milner type algorithm, which deduces types with 100% certainty and can prove it mathematically as long as your variables are immutable (and a few other language design choices). TypeScript on the other hand tries to do its best at finding a common type for all your usages.

This might blow your mind as a TypeScript user, but the following ReScript function is fully statically typed:

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

ReScript knows with provable certainty that a and b are both int and that the function returns an int. This is because the + operator only works on two int and returns an int . To concatenate two strings youd use ++ and for two floats use +.. To combine two different types you need to convert either of them. Also, no semicolons.

If youre like me and like to type your code as you prototype, you can do so as youd expect:

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

The generated JavaScript code in both cases is the same (ReScript v8.4.2):

'use strict';function add(a, b) {      return a + b | 0;  }exports.add = add;
Enter fullscreen mode Exit fullscreen mode

Notice how I didnt specify any module exports but the resulting code did. This shows how everything in the module/file is exported by default. The JavaScript function itself is not type safe, so importing it in a JavaScript module and using it there wont have all the advantages of ReScript.

You can try it for yourself in the official playground.

Generating TypeScript

To interoperate with TypeScript with proper type information youll use third-party genType. Add it as a devDependency and annotate the module export you want to generate with @genType (in previous versions youd surround annotations with square brackets).

// MyModule.res@genType  let add = (a,b) => a + b
Enter fullscreen mode Exit fullscreen mode

This will result in the following TypeScript. Notice how the generated TypeScript imports the generated JavaScript MyModule.bs.js file:

// MyModule.gen.tsxconst MyModuleBS = require('./MyModule.bs');export const add: (_1:number, _2:number) => number = MyModuleBS.add;
Enter fullscreen mode Exit fullscreen mode

GenType generates a one-line re-export of your generated .bs.js file, with proper TypeScript typing. From this example youll notice two more things:

  • Every file is a module.
  • Everything is exported.

Heres an example repo genTyping to TypeScript with React.

For using TypeScript types, see Importing TypeScript Types below.

Records

There is only one type which does need a type declaration, which is the record type. A type declaration will look like this and produces no JavaScript code:

type student = {    age: int,    name: string  }
Enter fullscreen mode Exit fullscreen mode

Types must begin with a lowercase! If we prepend it with @genType, the generated TypeScript will look like this:

// tslint:disable-next-line:interface-over-type-literal_  export type student = {      readonly age: number;      readonly name: string  };
Enter fullscreen mode Exit fullscreen mode

If youre wincing at the lower-cased type breaking all your conventions you can rename the type on conversion with @genType.as("Student"). This will add another line of code below the previous one:

export type Student = student;
Enter fullscreen mode Exit fullscreen mode

Also it includes a tslint ignore line, which I hope they switch soon to eslint as the former is deprecated.

These are record types, not ReScript objects (dont misuse the string type on them). As soon you type something like foo.age ReScript will know that foo is of type student. In case theres another record with and age field, it will infer its the last one declared. In that case you might want to explicitly annotate the type.

In the case you dont want that much ceremony, you can use the object type and index it with a string: student["age"]; then you dont need to declare a type.

Furthermore you can use student as a variable name, so student.age is a valid expression, TypeScript would scream at something like this. Variables (that is, bindings) and Types live in a separate namespace, so a student of type student an be written as student: student.

Nominal Typing

Record types have nominal typing similar to Java or C#, as opposed to TypeScripts structural typing. This is why interfaces are so important in TypeScript, and are used much more than Types. TypeScript doesnt really care about what you are, it cares about how you look.

For instance, if theres another type, say, teacher with the same fields of a student, you cannot assign a student to somewhere expecting a teacher:

// defined first  type student = {    age: int,    name: string  }// defined last  type teacher = {      age: int,      name: string  }// t is a teacher  let t = {      age: 35,      name: "Ronen"  }let s: student = t // Error!
Enter fullscreen mode Exit fullscreen mode

Youd get a colored error saying:

We've found a bug for you!//...This has type: teacherSomewhere wanted: student  FAILED: cannot make progress due to previous errors.  >>>> Finish compiling(exit: 1)
Enter fullscreen mode Exit fullscreen mode

Unlike TypeScripts tsc compiler, bsb wont begrudgingly continue its transpilation work into working JavaScript. It will stop with a non-zero exit code, and you have to fix the issue in order to make any progress.

Optionals

One of the features I most like in modern TypeScript (or future JavaScript) are the optionals. They make working with nullable types easy and concise:

const something: string = foo?.bar?.baz ?? "default";

something will be the content of baz if it reached that far, or be "default".

There are no null or undefined in ReScript. But we can work with nullable values using the Variant option. But how can we get the elegance of the above TypeScript code? I tried to answer this question but, we cant, currently. Not enough sugar.

As with other functional languages, we can use a myriad of interesting library functions. Some of Belt utility functions are:

  • Belt.Option.Map will execute a function on the optional value if it exists, or return None.
  • Belt.Option.getWithDefault will return a default if the optional is None.
  • Belt.Array.keepMap will trim away all None values from an array.

But for this case, the best option is with Pattern Matching:

let baz = switch foo {     | Some({ bar: Some({ baz: baz })}) => baz     | None => None  }
Enter fullscreen mode Exit fullscreen mode

There isnt yet a sugared syntax for optionals; the optional operators are very new to TypeScript as well.

The important quality of pattern matching is that the compiler will complain if theres any case doesnt matter how deeply nested you havent addressed. Its best practice for most cases.

Pipes

Pipes are great. They compile this code:

person    ->parseData    ->getAge    ->validateAge
Enter fullscreen mode Exit fullscreen mode

Into this:

validateAge(getAge(parseData(person)));
Enter fullscreen mode Exit fullscreen mode

Previous versions used a triangle operator |>. The difference is in where to shove the data: as the first parameter, as the arrow does, or as the last parameter, as the deprecated triangle does. More about this.

Notice that in the case of a one-parameter function we dont write the unit, that is (). This is a common beginners mistake. In the case of multiple parameters, the value gets passed as the first one and the other parameters begin with the second one.

This is especially important in a functional language, since we lose some of the elegance of calling methods in objects.

What would be a JavaScript method call such as map:

myArray.map(value => console.log(value));
Enter fullscreen mode Exit fullscreen mode

Has to be written functionally in ReScript as:

Belt.Array.map(myArray, value => Js.log(value))
Enter fullscreen mode Exit fullscreen mode

But can be rewritten as:

myArray -> Belt.Array.map(value => Js.log(value))
Enter fullscreen mode Exit fullscreen mode

As a newcomer I try to find a use for it anywhere I can, which can lead to the bad practice of rewriting code around it to impress my coworkers. To use it on JavaScript libraries youll have to write the correct bindings for them. This is one thing Id like to see in JavaScript. Here are a few stage-1 proposals.

By the way, if youre not using Fira Code then youre missing out on a lot of the pipes aesthetics.

Promises

This was very frustrating for me. I love using modern async and await syntax in my code, which ReScript didnt implement yet. I had to go back into thinking about then and resolve, which made simple code look complex.

The following code:

const getName = async (id: number): Promise<string> => {      const user = await fetchUser(id);      return user.name;  }
Enter fullscreen mode Exit fullscreen mode

Is de-sugared into:

const getName = async (id: number): Promise<string> =>       fetchUser(id).then(user => user.name);
Enter fullscreen mode Exit fullscreen mode

Now consider then to be a function in the Js.Promises module instead of a method, which accepts fetchUser(id) as its last parameter, and you can write it like this:

let getName = (id) =>      Js.Promise.then_(          user => Js.Promise.resolve(user.name),          fetchUser(id))
Enter fullscreen mode Exit fullscreen mode

Typed as Js.Promise.t<string>, and with arrow pipe syntax for readability, the above function can be written as:

let getName = (id): Js.Promise.t<string> =>      fetchUser(id) |> Js.Promise.then_(          user => Js.Promise.resolve(user.name))
Enter fullscreen mode Exit fullscreen mode

The Promise library still uses the old convention of passing the data as the last argument, so in order to use the newer arrow pipe, an underscore has to be placed in the proper location.

Here are examples for Promises written in the (almost-identical) ReasonML syntax.

The ReScript team promised (no pun intended) to implement a Promise API revamp with their own async and await.

Import JavaScript Modules

If youre writing only in ReScript you dont need to bother with imports or exports, and this is done under the hood. Every file is a module and everything in it is exported. If you only want specific things exported you do so with an interface file. To import JavaScript modules however, the syntax can get complicated.

To import dirname from the path module, youd write:

@bs.module("path") external dirname: string => string = "dirname"

Image for post

the elements of an import from JavaScript files

Then use it accordingly:

let root = dirname("/User/github") // returns "User"
Enter fullscreen mode Exit fullscreen mode

For ReasonReact this became particularly tiresome, as I had to define inline modules for each React Component, and reexport the default export as the make function, paying attention to named parameters such as children. Here I imported the Container from react-bootstrap and used it in ReasonReact:

module Container = {      @bs.module("react-bootstrap/Container")      @react.component      external make: (~children: React.element) => React.element = "default"  }@react.component  let make = () => <Container> ...
Enter fullscreen mode Exit fullscreen mode

Redex

REDEX: Reason Package Index

For this case I can get the bindings from redex, and add it as a dependency both to my package.json and my bsconfig.json. I can then import it with open ReactBootstrap at the top of my file. This is similar to DefinitelyTyped, where you can find high-quality type definitions for TypeScript.

For this case however I ran into an error, as the package I needed was not updated to the latest version. I had to fork it and manually update it to react-jsx version 3.

Importing TypeScript Types

You cant import a type from TypeScript and use it in ReScript, you have to re-declare it. However, you can link the type you created to the original TypeScript one for correct inter-operation. Heres an example with Node.js fs module:

@genType.import(("fs", "Dirent"))  type dirent
Enter fullscreen mode Exit fullscreen mode

Notice that I passed a tuple to import, not an argument list. This will link my type dirent to fs.Dirent, and will generate the following TypeScript:

import {Dirent as $$dirent} from 'fs';_// tslint:disable-next-line:interface-over-type-literal_  export type dirent = $$dirent;
Enter fullscreen mode Exit fullscreen mode

You can declare the entire type, in case you need to use its properties, or leave it as is.

Because of the syntax overhead of TypeScript-ReScript inter-operation, I recommend doing it as little as possible, using each language in separate areas of your app.

ReasonReact

Image for post

ReasonML (now ReScript) was created by Jordan Walke, the creator of React. Reason+React pushes the React philosophy further by utilizing the language syntax and features for ReactJSs programming patterns.

ReasonReact provides smooth JS interop and uses built-in language features to integrate into UI framework patterns left unaddressed by ReactJS, such as routing and data management. Using them feels like just using Reason.

The documentation for ReasonReact still uses the old syntax, so things like:

[@react.component]
Enter fullscreen mode Exit fullscreen mode

Needs to be changed into:

@react.component
Enter fullscreen mode Exit fullscreen mode

If you want to use the old syntax, just change the file extension to .re instead of .res.

ReasonReact is stricter than ReactJS, mainly in its use of types (e.g., strings need to be used with React.string()in JSX. Other than this, the React.useState returns a proper tuple instead of an array, the way it was originally intended. Finally, React Components are rendered through a make function, and prepended with @react.component (I added @genType as well for TypeScript generation):

For the example, I imported this component into a React TypeScript file:

// index.tsximport { make as Demo } from "./pages/Demo.gen";// ...<Demo name={"Foo"} />
Enter fullscreen mode Exit fullscreen mode

Which, when rendered, looks like this:

Image for post

In case we dont want GenType for TypeScript generation, we just import Demo.bs instead.

Testing

In order to write tests in ReScript, and thus test your code directly, you can use bs-jest, which provides ReScript bindings to Jest. If you prefer, you can also use the slightly less mature bs-mocha. You can also test the generated JavaScript or TypeScript files with no extra configuration.

Since ReScript is in the JavaScript ecosystem it makes little sense to create specialized testing tools for ReScript, and the direction seems to be in developing bindings for JavaScript testing tools.

With bs-jest, you have to name you cant name your file foo.spec.res, only with a valid module name, such as foo_spec.res. Jest will run on the compiled folder, by default inside lib/js. Also, assertions are not executed immediately, but instead returned by the function and run at the end of the suite. Its a functional way to thing about tests. Consequently, you can only write one assertion per test, which is best practice anyway.

Tooling

ReScript devs did well on prioritizing the plugin for VSCode, which works really well. With the ReScripts watcher running, youll see your Type errors underlined in red, with a descriptive bubble on hover. You also get type hints, formatting, and jumps to definitions. Theres also official support for Vim (both plain Vim and Coc Language Server) and Sublime.

Image for post

Screen capture from rescript-vscode.

The Community

A few times in my coding career I had to work with small communities, and I always loved it. I developed smart-contracts in Solidity, some database queries in the functional language Q, and Roku channels in BrightScript. You end up working with Slack/Discord/Gitter open and code together with the few others going through your similar problems. You dont even bother checking StackOverflow for answers.

This forces you to read and reread the official documentation and examples, as you dont want to look like dumb in the chatroom. Also, youre part of a community maintained by real people, where you can always contribute something interesting, and even shape its development.

Not all communities are alike, of course. I personally found the ReasonML/ReScript community to be welcoming. ReScript has an official forum where you can communicate asynchronously and with a permanent paper record you can search. The core team consists of a handful of developers with public Twitter accounts, and theres an official blog. I found however that the community hangs around in the ReasonMLs Discord server, in an unofficial ReScript room.

Finally, theres ReasonTown, a podcast about the ReasonML language and the community that makes it good, ReasonConfs YouTube channel, and Redex, to find bindings for your libraries.

Conclusion

The switch is not easy; a refactor of an existing app is even more difficult given its fatal stop on the first issue. This will certainly hinder its adoption. Popular transpilers, such as TypeScript, SCSS, or CoffeeScript garnered adoption by its ease. Just copy-paste your code or rename your file and youre done.

This is different. ReScript, as with other statically typed functional languages, aims at changing the way code is approached at a fundamental level. I believe well see a greater adoption of functional programming in the future, eventually becoming the default for some industries. This is due to the mathematical approach to types, formal verification of a programs correctness, and given immutability: less moving pieces and mental mapping.

Were already at the first stage of adopting a functional style in the ecosystem with map, filter, reduce functions in JavaScript. ReScript represent the next hybrid stage of a properly functional language from the ML family which compiles to the industrys standard JavaScript.

Functional programming at its core takes itself seriously. Its mathematical, formal, and doesnt comply with hacks. It aspires to deals with truths, not processes. Writing a functional style in JavaScript only whets ones appetite for more, as the language brings ones good intentions down, not up. ReScript, while frustrating, might be the precision tool for a more civilized future in the ecosystem.


Original Link: https://dev.to/ronenl/how-i-switched-from-typescript-to-rescript-1g34

Share this article:    Share on Facebook
View Full Article

Dev To

An online community for sharing and discovering great ideas, having debates, and making friends

More About this Source Visit Dev To