TypeScript vs ReasonML – A Comparison
September 01, 2019
Both TypeScript and ReasonML claim to offer statically typed language for web developers that compiles to JavaScript. But there are important differences. TypeScript’s best and the worst feature is that is a superset of the JavaScript. And while having that familiarity with JavaScript is nice, it means that every quirkiness that we love and hate about JavaScript is still there in TypeScript. The types are added on top of the JavaScript and that works, kind of.
What ReasonML offers is a completely different yet familiar language. Different language means it will be hard to learn for JavaScript/TypeScript developers? Well, let’s take a look, shall we?
Declaring a variable
Let’s start from a variable declaration
const a = "Hi"
let a = "Hi";
In ReasonML we declare a variable with let
keyword. There’s no const, let
is immutable by default.
Both languages inferred type of a
in this situation
Functions
let sum = (a: number, b: number) => a + b
let sum = (a,b) => a + b;
Although I didn’t write any types by hand, the arguments of this function are typed.
Why don’t we have to write types in ReasonML? Because of the powerful type system that allows for incredible type inference.
What it means is that compiler can deduce types without your help. (+)
operator in ReasonML works only with integers — the only type that a
and b
can be, so we don’t have to write them.
But you always can write types, if you want:
let sum = (a: int, b: int) => a + b;
Interfaces, Records
interface Product {
name: string
id: number
}
The closest thing to interface in ReasonML is a record
type product = {
name: string,
id: int,
};
Records are like TypeScript objects but are immutable, fixed and a bit more rigidly typed.
Let’s use defined structures in some function
const formatName = (product: Product) => "Name: " + product.name
let formatName = product => "Name: "++product.name;
Again, we don’t need to annotate types! In this function, we have an argument product
that has a property name
of type string.
ReasonML compiler can guess the type of that variable based on usage. As the only type that has name
property of type string is product
compiler will infer it.
Updating records
const updateName = (product: Product, name: string) => ({ ...product, name })
let updateName = (product, name) => { ...product, name };
ReasonML supports spread operator and punning for name and values just like TypeScript
It is also interesting to look at what javascript was generated by ReasonML
function updateName(product, name) {
return [name, product[1]]
}
Records in ReasonML are represented as arrays. And generated code looks like a human wrote it if humans could remember indices of every property of every type like a compiler does.
Reducer example
This is, in my opinion, is where ReasonML really shines. Let’s compare implementations of the same reducer:
in TypeScript (following this guide https://redux.js.org/recipes/usage-with-typescript)
interface State {
movies: string[]
}
const defaultState: State = {
movies: [],
}
export const ADD_MOVIE = "ADD_MOVIE"
export const REMOVE_MOVIE = "REMOVE_MOVIE"
export const RESET = "RESET"
interface AddMovieAction {
type: typeof ADD_MOVIE
payload: string
}
interface RemoveMovieAction {
type: typeof REMOVE_MOVIE
payload: string
}
interface ResetAction {
type: typeof RESET
}
type ActionTypes = AddMovieAction | RemoveMovieAction | ResetAction
export function addMovie(movie: string): ActionTypes {
return {
type: ADD_MOVIE,
payload: movie,
}
}
export function removeMovie(movie: string): ActionTypes {
return {
type: REMOVE_MOVIE,
payload: movie,
}
}
export function reset(): ActionTypes {
return {
type: RESET,
}
}
const reducer = (state: State, action: Action) => {
switch (action.type) {
case ADD_MOVIE:
return { movies: [movie, ...state.movies] }
case REMOVE_MOVIE:
return { movies: state.movie.filter(m => m !== movie) }
case RESET:
return defaultState
default:
return state
}
}
Nothing crazy here, we declared state interface, default state, actions, action creators and finally reducer.
and now the same thing in ReasonML
type state = {
movies: list(string)
};
type action =
| AddMovie(string)
| RemoveMovie(string)
| Reset
let defaultState = { movies: [] }
let reducer = (state) => fun
| AddMovie(movie) => { movies: [movie, ...state.movies] }
| RemoveMovie(movie) => { movies: state.movies |> List.filter(m => m !== movie) }
| Reset => defaultState;
/* No need for additional functions! */
let someAction = AddMovie("The End of Evangelion")
Yep, that is it.
Lets look at what is happening here.
First, there is a state type declaration.
After that is action Variant type
type action =
| AddMovie(string)
| RemoveMovie(string)
| Reset
What is means that any variable with the type action
can have one of the following values: Reset
,
AddMovie
with some string value and RemoveMovie
with some string value
The Variant is a very powerful feature that allows us to define a type that can have values A or B in a very concise way. Yes, TypeScript has union types but it doesn’t have the same level of integration with language because types were kind of patched to the JavaScript to make TypeScript but Variants are an essential part of the ReasonML language and exists side by side with other language features like pattern matching.
Speaking of pattern matching, let’s look at the reducer.
let reducer = (state) => fun
| AddMovie(movie) => { movies: [movie, ...state.movies] }
| RemoveMovie(movie) => { movies: state.movies |> List.filter(m => m !== movie) }
| Reset => defaultState;
What we see here is a function that accepts the state as a first argument and then matches the second argument to possible values.
We can also write this function like this:
let reducer = (state, action) => {
switch(action) {
| AddMovie(movie) => { movies: [movie, ...state.movies] }
| RemoveMovie(movie) => { movies: state.movies |> List.filter(m => m !== movie) }
| Reset => defaultState;
}
}
As matching an argument is a common pattern in ReasonML this kind of functions are written in a shorter format like in the previous code snippet.
Parts like this — { movies: [movie, ...state.movies] }
look the same as in TypeScript, but what is happening here is not the same!
In ReasonML [1,2,3]
is not an array but an immutable list. Imagine it as having Immutable.js
built-in into the language itself.
In this part, we are taking advantage of the fact that the append operation has constant time in ReasonML lists!
Coming from JavaScript or TypeScript you might write something like this without much thought and you’re getting performance improvements for free.
Now let’s look at the experience of adding a new action to this reducer. How does that look like in TypeScript? Firstly, you add a new action to type definition then write some boilerplate in form of action creators and yeah don’t forget to actually handle that case in the reducer which is who knows where.
In ReasonML first step is exactly the same but the next steps are anything but. As you hit save after adding a new action to the type definition you are led by the compiler on a gentle stroll across your codebase to handle cases accordingly
You’ll see a warning like this:
Warning 8: this pattern-matching is not exhaustive.
Here is an example of a case that is not matched:
Sort
Now, this is good developer experience. It will point you to the exact place where you need to handle new action and will also tell you exactly which case is missing.
Null, undefined vs Option
In TypeScript we are burdened with JavaScript legacy of having both null
and undefined
for representing
almost the same thing - an absence of value.
In ReasonML there’s no such thing, there’s only Option
type.
type option('a) =
| Some('a)
| None;
This is an already familiar variant type. But it also has a type parameter 'a
. This is similar to generics in other languages like Option<T>
.
Let’s compare more code
interface User {
phone?: number
}
interface Form {
user?: User
}
function getPhone(form: Form): number | undefined {
if (form.user === undefined) {
return undefined
}
if (form.user.phone === undefined) {
return undefined
}
return form.user.phone
}
Accessing nullable properties is one of the simplest cases where you can shoot in your foot. In TypeScript, we can remedy it by enabling strict null checks and then checking manually for undefined values.
open Belt.Option;
type user = {
phone: option(int)
};
type form = {
user: option(user)
};
let getPhone = form =>
form.user->flatMap(u => u.phone);
In ReasonML we can use built-in option
type and together with helper functions from Belt standard
library we can handle possibly empty values in standardized way
Labeled arguments
I don’t think that anyone will argue that the labeled arguments feature is just plain awesome. Probably everyone has had to look up the order or the meaning of the function’s arguments at some point. Unfortunately, there are no labeled arguments in TypeScript.
function makeShadow(x: number, y: number, spread: number, color: string) {
return 0
}
const shadow = makeShadow(10, 10, 5, "black") /* meh */
In ReasonML you put a ~
character before argument’s name and it becomes labeled.
let makeShadow = (~x: int, ~y: int, ~spread: int, ~color: string) => {
0;
}
let shadow = makeShadow(~spread=5, ~x=10, ~y=10, ~color="black")
And yes, you can try and emulate it with an object as an argument in TypeScript, but then you’re allocating an object at every function call :/
function makeShadow(args: {
x: number
y: number
spread: number
color: number
}) {
return 0
}
const shadow = makeShadow({ x: 10, y: 10, spread: 5, color: "black" })
Module system
In TypeScript we explicitly export and import anything from file to file.
export const test = "Hello"
import { test } from "./Hello.ts"
console.log(test)
In ReasonML every file is a module with the name of that file.
let test = "Hello";
Js.log(Hello.test);
You can open
modules which makes content available without module name prefix
open Hello;
Js.log(test);
Compiling speed
To compare compile speed let’s compile TodoMVC, as implementation and therefore project size is roughly comparable. What we are measuring is the time it takes to transpile code to JavaScript. No bundling, minifying, etc. [1]
$ time tsc -p js
tsc -p js 6.18s user 0.24s system 115% cpu 5.572 total
6.18 seconds
$ bsb -clean-world
$ time bsb -make-world
[18/18] Building src/ReactDOMRe.mlast.d
[9/9] Building src/ReactDOMRe.cmj
[6/6] Building src/Fetch.mlast.d
[3/3] Building src/bs_fetch.cmj
[12/12] Building src/Json_encode.mlast.d
[6/6] Building src/Json.cmj
[7/7] Building src/todomvc/App.mlast.d
[3/3] Building src/todomvc/App-ReasonReactExample.cmj
bsb -make-world 0.96s user 0.73s system 161% cpu 1.049 total
0.96 seconds
Now this also includes compiling ReasonML dependencies, we can measure compiling time of only project files
$ bsb -clean
$ time bsb -make-world
ninja: no work to do.
ninja: no work to do.
ninja: no work to do.
[7/7] Building src/todomvc/App.mlast.d
[3/3] Building src/todomvc/App-ReasonReactExample.cmj
bsb -make-world 0.33s user 0.27s system 117% cpu 0.512 total
0.33 seconds
Now that is fast!
BuckleScript considers performance at install time, build time and run time as a serious feature
This is the quote taken from the BuckleScript documentation. BuckleScript is the tool that transpiles ReasonML to JavaScript.
Where TypeScript beats ReasonML (for now)
Not everything about ReasonML is flowers and butterflies. It is a fairly new language… Well, actually no it is based on OCaml which is fairly old but the point is that there are still not a lot of resources online. Googling TypeScript question will more likely to yield an answer than a ReasonML one.
DefinitelyTyped typings amount is just insane and it will take time for ReasonML to match them.
Closing remarks
ReasonML syntax should be really familiar for front-end developers which means that the beginning of the learning curve is not that steep (But still steeper than TypeScript). ReasonML takes the best things from the other languages and tools like Immutable.js, eslint and brings it to the language level. It doesn’t try to be a completely pure programming language, you can always fallback to mutation and imperative programming when you need it. It is crazy fast which is a big part of great developer experience. ReasonML is everything TypeScript tries to be (and a bit more) without all that JavaScript weirdness. You should try it!
[1] Compile time tests performed on a Mid-2015 MacBook Pro with Intel(R) Core(TM) i5-5287U CPU @ 2.90GHz
TypeScript + React.js source code
ReasonML + ReasonReact source code
Blog by Oleksandr Dubenko