Gaurab Paul

Polyglot software developer & consultant passionate about web development, distributed systems and open source technologies

Support my blog and open-source work

Tags

ReasonML vs TypeScript – First impressions
Posted  6 years ago

I don't actively use ReasonML and this post has not been since it was originally written. So some or more of the content here may be outdated.

This post summarizes preliminary observations while comparing ReasonML and TypeScript during selection of a reasonably (pun-intended) type safe language for frontend development. The observations here are somewhat biased in favor of experienced javascript developers and focusses primarily on frontend development workflow and does not take into account the (primary) native backend of Reason.

While this post primarily compares Reason and TypeScript, much of what is outlined about TypeScript equally applies to flow as well.

Compatibility with ECMAScript semantics

TypeScript was designed from ground up to be a javascript superset. So all javascript experience naturally carries over to TypeScript.

Reason is an OCaml dialect. A different language that has nothing to do with ECMAScript. period.

This gets slightly blurred because of Reason team’s efforts to bring in some javascript inspired syntax sugar to OCaml. So lists look like javascript arrays, records look like javascript objects, functions look like javascript arrow functions, spread syntax looks the same etc. But these syntactic similarities are very much skin deep and while they may aid in muscle memory, one has to keep in mind while writing any non-trivial code that Reason is a different language with very different semantics. We will revisit these semantic differences at multiple places in the later sections of this post.

However it is worth highlighting that most of the ReasonML best practices also map to best practices in Modern javascript. The official documentation elaborates on this in the What & Why section that explains the motivation behind the ReasonML.

When we say that we are comparing TypeScript with Reason in this post, we are actually comparing TypeScript with the combination of OCaml (The underlying language which provides the type system) + BuckleScript (The OCaml -> Javascript compiler, a fork of the primary OCaml compiler) + Reason (The javascript inspired dialect for OCaml – implemented as a frontend for the OCaml compiler).

Type safety

TypeScript is intentionally “gradually-typed”. TypeScript compiler supports various levels of strictness which can be configured through the tsconfig.json file.

In loose mode, TypeScript can be freely intermixed with untyped javascript and the compiler will do a best-effort to detect as many type violations as it can, and embrace all untyped data as “any” type which is simply not type-checked.

For example given the following definition:

Compiler infers the type of fn as follows:

So all that the TypeScript compiler knows about fn is that it is a function and that it takes two arguments. There is no type safety around the parameters, so the compiler will happily accept a usage like:

We also see that any is contagious. Given that the parameters are untyped, any results that depend on these parameters also has any type.

In strict mode, the compiler will warn when variables end up with any type implicitly. But still, the developer has enough flexibility to explicitly use any whenever they see fit.

There is also a @ts-ignore pragma which simply disables type checking for annotated sections of code.

This can be somewhat advantageous wrt. interoperability because many prevalent javascript usage patterns can not be easily type-checked by the compiler and typescript intends to be a javascript superset so it has to accomodate for all kinds of dynamic and stringly typed APIs.

Example from lodash library:

If we look at the type definition for set:

TypeScript will not make an attempt to validate that after setting the value, whether or not the resultant object still complies with the interface T and will just assume that it will.

OCaml was designed to be a strongly typed language from the get go, and does not allow these kind of leeways. The type system is intentionally sound and type coverage is guaranteed to be 100%.

For interop, BuckleScript compiler supports typed externals which act as type hints for the compiler and are erased afterwards. Examples from BuckleScript docs:

As a “last-resort escape hatch” embedding “raw” untyped javasacript is also possible.

The type of x here is akin to any in typescript which “unifies with everything”, and is highly discouraged.

It is recommended that whenever javascript values are pulled in from the “unsafe world”, they are explicitly typed at the interop boundaries so that type safety is not compromised.

TypeScript also supports a zero-commitment opt-in type checking through type annotations in comments.

This is advantageous for large javascript projects to ease gradual migration to typescript. And also when a compilation step is not desirable.
However, as with all solutions which abuse comments for purposes other than commenting, this is prone to human errors eg. typos in your type declarations may end up getting ignored by the compiler etc.

In contrast, Reason enthusiasts take the stance that you really value type safety and so are willing to make the commitment and opt into an entirely different language.

Type inference

OCaml is quite famous for its sophisticated type inference, and is much better at this than typescript. When starting out, type inference in Reason can feel almost magical.

In the code below:

The printName function can infer the type of p based on its usage within the function body.

In contrast, TypeScript infers all untyped function arguments as any.

Inference can be nice, but in case of type mismatch, the errors can sometimes be misleading wrt. whether the error is at call site or the definition site.

In last year or so, there has not been a single occurance where I have encountered a TypeScript error and have not been able to comprehend within 5 mins what the error exactly is.

Type directed emit

One of the primary design decisions of typescript is to avoid type directed emit. From Non-goals section:

[TS will not] Add or rely on run-time type information in programs, or emit different code based on the results of the type system. Instead, encourage programming patterns that do not require run-time metadata.

The consequence of this is that the generated javascript is fairly close to handwritten idiomatic javascript. This also has advantage in that opting out of TypeScript is relatively easy.

Reason (or rather BuckleScript) has no such qualms. A lot of optimization efforts have gone into BuckleScript to fully utilize the type system and take advantage of compiler’s intelligence to generate tiny compilation outputs.

In contrast TypeScript explicitly states that aggressive optimization is not one of compiler’s goals. From the Non-goals section in the roadmap:

[TS will not] Aggressively optimize the runtime performance of programs. Instead, emit idiomatic JavaScript code that plays well with the performance characteristics of runtime platforms.

It is to be noted though, that statically analyzable code can often be better optimized by the JIT compiler or optimizers like UglifyJS or Google Closure.

An example of reason’s optimization can be seen in how it implements records:

One might have expected the person type to be represented as a javascript class, but the compiler optimizes away the record type entirely and leaves behind a compact array.

The negative side of this is that there it is that scope of runtime reflection is severly limited. Runtime reflection is typically not idiomatic in OCaml. The metaprogramming section below touches upon some of the alternatives.

Functional Programming

Javascript has many functional features, primary among them is support for higher order functions. These features carry over seamlessly in TypeScript. TypeScript’s type system handles higher order functions etc. quite well, but besides that it does little on top to encourage functional programming.

There are many caveats of functional programming in javascript, primary among them is that the built-in data structures are not really designed with immutability in mind.

Adopting immutability in javascript boils down to one of these two options:

  1. We can use helper libraries to operate on native data structures without mutations. This often requires unnecessary cloning though, and is, in general, not particularly efficient.

  2. We can use something like ImmutableJS, but most libraries accept native arrays and objects, in such cases cost of coverting to and from and immutable can add up significantly.

Also, while functional composition is possible, the language could certainly benefit from more syntax sugar to make operations on functions as succinct as operations on primitives.

Reason has been designed with functional programming at its core. Though it has some object oriented features, it is primarily a functional language.

Primary built-in data structures are immutable, so it can be expected that all reason libraries play well with them. This includes lists, records, tuples etc. Remember that we said above that Reason’s built in data structures may quack like Javascript but have different semantics ?

BuckleScript has a number of optimizations in place to make operations on immutable entities performant.

Mutability is opt in and explicit (through usage of ref cells).

A few primary features of ReasonML that aid in functional programming are highlighted below. The 2ality blog by Axel Rauschmayer already introduces them very well, so I will defer to that post for elaborations.

Curried functions

Reason functions are curried by default. While this may seem weird to javascript developers (or developers from object oriented language backgrounds), this is really common in functional world, and partial application is quite common in practice. Hugh Jackson has written a great post on the advantages of currying.

FP enthusiasts have long advocated for currying in javascript and libraries like Ramda have embraced it throughout their API.

BuckleScript optimizes curried functions to prevent unnecessary use of closures wherever possible:

Compiles to:

So when incrementAgeBy is called with two arguments there is no overhead.

Side effect of implicit currying is that, in Reason we don’t have functions with variadic arguments, which is very common in javascript.
TypeScript supports variadic arguments of same type, but the only way to handle heterogenous variadic arguments is through any.

It is a common practice among libraries to handle few arities with generic types and falling back to any for higher index arities.

Example from MobX state tree

Currying also encourages “data-last signatures” where the primary subject of the operation is the last argument.

For cases, where is this is not adequate, Reason also supports labeled arguments. Labeled arguments also aid towards readability and while a similar thing can be emulated in javascript through object arguments and destructuring, in case of labeled arguments there is no overhead of an additional object allocation.

Compiles to:

Operators as functions.

Unlike javascript and typescript, OCaml offers custom operators. Operators are essentially functions with syntax sugar for infix/prefix usage.

When misused this can make code a lot more difficult to grok, but for commonly used patterns it can help towards reducing unnecessary verbosity in code. This can also aid towards embedded DSLs.

Pipe Operator

ReasonML supports a pipe operator for reverse application of functions.

Example from the linked post from 2ality blog:

This makes chaining of operations very easy to grok and helps with functional data transformation pipelines. As should be obvious from the example above, reverse application naturally fits in with support for punning and currying.

There are currently two competing proposals for incorporation of pipe operator in javascript and respective champions are currently debating whether to make this more similar to flow or hack. TypeScript support for this is nowhere to be seen yet.

Pattern Matching

Pattern Matching is the crown jewel of ML. It is amazingly flexible, widely used and results in very clean code. This also goes hand in hand with destructuring support. It is not a coincidence that the single most prominent code example in Reason home page illustrates pattern matching:

If you have ever used convoluted nested if-else conditionals and have wondered if there could be a better construct, which could support a lower boilerplate unified syntax for branching based on type of the subject, or a member of a collection, or a property of a record — well that is what pattern matching is, and it is available in all its glory in ReasonML right now.

As is to be expected, there is also a proposal for incorporating pattern matching in javascript. Once again, given the stage 0 status, TypeScript support is nowhere to be seen yet.

Pattern matching and algebraic data types go hand in hand.

Asynchronous Programming

It is fairly easy to use callbacks and promises when interoperating with javascript code. BuckleScript already provides bindings to javascript promises.

TypeScript has had, for a long time, first class support for ES7 async/await keywords. There have been some attempts to rebuild similar support in Reason in Reason, but as the author has highlighted in the linked post, the flexibility of javascript promises makes exact emulation of async/await difficult to play well with Reason’s strict typesystem.

The good news is that since all Javascript async APIs use promises as the foundational abstractions, interop through Promises is quite easy.

The various flavors of object types

Typescript embraces structural typing which is a natural fit for interoperating with a dynamic language. Jamie Kyle has provided a good introduction on the differences between Structural and Nominal type systems.

TypeScript’s interfaces are amazingly flexible and easy to understand. Along with Union and Intersection types, they offer a wide berth for modelling polymorphic behavior. And they can be used to model almost all statically analysable usage patterns in javascript.

In contrast, in Reason we have Records (nominally typed), Objects (structurally typed) and BuckleScript’s wrapped Objects which have different semantics.

While records are much faster implementation wise, they are rigidly typed: The types have to be declared beforehand and there is little support for deriving record types from other record types. The differences and subtly different syntax of these can be very confusing when starting out. We have already seen usage of Records in previous parts of this article and observed that BuckleScript compiles them away to compact arrays.

So now the question arises, what if you really need a javascript object ? What if we want to have methods with a shared context (this in javascript) ?

That is where the object type comes into picture. Before we get into them, let us try to identify two common usage patterns around objects in javascript :

Objects as Maps vs Objects as Records

In javascript, the enormous flexibility of objects have lent them to be used as both dictionaries (A collection of arbitrary key to value mappings) as well as Records (Mapping from fixed set of keys to values of priorly known types).

TypeScript handles the former case through interfaces having a string index signature:

and the latter case through explicitly typed interface properties:

It is easily possible and common to mix and match these two usage patterns.

BuckleScript strives to clearly distinguish between these two usage patterns. For dictionary style usage, JS.Dict is preferred. And for Record style usage BuckleScript provides a wrapper to be used in conjuction with Object types.

ReasonML Object Type

To see an example of object types, let us look an a translated example of stack object type from Real world OCaml:

Note that we didn’t have to declare the type of the return value of stack beforehand. Objects are less rigidly typed in this regard wrt. records.

If we look at the inferred type of s, there is something interesting to be observed:

No, the dot after the curly brace is not an error and that is how Reason distinguishes between Record Type and Object Types. Don’t you love the syntax already ?

The compiled output for this might be very surprising:

What has happened here is that Object Orientation in OCaml follows very different semantics than javascript’s prototypal inheritance, and BuckleScript by default uses an implementation that is faithful to native OCaml semantics.

However most of the time, when in javascript world, we would want to use javascript objects, and for that BuckleScript provides a wrapped object type which compile to javascript objects.

While the linked BuckleScript docs cover wrapped object type in detail, I want to highlight the syntax sugar provided by Reason to sweeten the usage of BuckleScript’s wrapped object type :

The above is a valid code and compiles to exactly what you’d expect:

Note that unlike JavaScript/TypeScript, the quotes here are significant (Otherwise it would be treated as a Record). Remember I noted above that syntactic similarities are only skin deep ?

If we want this object to be explicitly typed, we can specify the object type as follows:

At this point I guess the {. seems quite intuitive. No ? Oh well …

When it is at all not possible to fit usage of objects into one of the above two usage patterns, there is also support for a more dynamic usage pattern which is generally discouraged.

Modules

ES6 Modules are very different from how most languages implement module systems. For the most part, I am a big fan of the one-to-one correlation between modules and files.

I am also a big fan of explicit imports in ES6 as they make it very easy to follow through logic when doing code reviews in github.

The typescript tooling is so good at this point, that I rarely have to write imports by hand. I can, in almost 100% cases, rely on typescript language server to identify and inject imports in source files and I heavily rely on tools for most of my refactoring.

Reason/OCaml modules are much much different and in some ways more advanced.

Modules are identified and referenced by names and not by relative paths to modules.

While every file implicitly does define its own module, files can have many explicitly defined modules as well.

In case of implicitly defined modules, the module name is derived from the name of the module. The location of the file in the filesystem hierarchy is insignificant and files can be moved around without needing any refactoring. This implies that verbose file names are quite common.

As explained by Yaron Minskey :

In particular OCaml has no good way of organizing modules into packages. One sign of the problem is that you can’t build an executable that has two modules with the same module name. This is a pretty awkward restriction, and it gets unworkable pretty fast as your codebase gets bigger

The post linked above recommends using module aliases as a namespacing alternative.

OCaml is fairly unique in its support of parameterized modules or Functors which can significantly aid in type-safe code reuse and can serve as foundation for sophisticated abstractions. Fuctors is a fairly large topic in itself and deserves a dedicated post in this blog, which will follow in near future.

As the official documentation explains:

Modules and functors are at a different “layer” of language than the rest.

Explicit modules are somewhat akin to TypeScript namespaces, but support for module parameterization does not have a direct equally powerful analogue in TypeScript.

Meta programming

The work around ppx is arguably one of the most interesting aspects of the OCaml ecosystem.

Quoting from the linked site:

The PPX language feature provides a new API for syntactic extensions in OCaml. PPX uses attributes to allow type-driven code generation, extension nodes for custom constructs and quoted strings for insertion of code fragments in unrelated syntax

TypeScript has a compiler API to support similar use cases which facilitates programmatic AST traversal and manipulation. However, as of this writing, this is marked as experimental and is not as mature.

TypeScript also has experimental support for exposing design time type information at runtime through metadata reflection which facilitates runtime metaproramming.

JSX

Given that Reason is by facebook, it is no surprise that their love of embedded XML-ish DSL carries over (from JSX, XHP etc.) and thus Reason is blessed with a similar dedicated syntax sugar.

There are subtle differences from what happens in javascript, esp. punning, but overall the superfluous verbosity of the XML inspired DSL disgusts me in either languages, so not much to add here.

Interop

Bucklescript has a dedicated section on Interop which covers most use cases. But the tl;dr is that while unlike TypeScript not all javascript dynamic usage patterns can be expressed directly in OCaml. But through BuckleScript’s embedded javascript support it is possible to drop down to untyped javascript and wrap such usages and expose them in a type safe manner.

Bucklescript’s interop support is somewhere between Elm’s port system (which is the most tedious of all interop solutions I have encountered) and TypeScript which is happy to let you intermix untyped and typed javascript code.

However this also implies that Bucklescript can’t make strong guarantees about zero runtime errors as Elm can. Of course, TypeScript’s unsound type system has never had any such guarantees and nor is this planned as per their Roadmap.

Caveats around esoteric syntax

Javascript is well know to have many “warts” around its weak typing.

OCaml’s type system may be safe and sound, but it has its own set of quirks. Many of these bizarre syntax rules have been “unified” by Reason’s syntax sugar. However some of them are very fundamentally rooted into the type system.

We have already seen the interesting use of operators and quoting in objects above. One such quirk that is most commonly visible is that (as mentioned above) operators are just functions and functions don’t have method overloading so we must use different operators when operating on different types.

For example we need different operators when doing operations on float and integers:

*., +. are float multiplication and addition operators respectively.

Adhoc polymorphism and typed languages have had a love hate relationship. Sophisticated type systems like Haskell handle them through features like Type Classes. Dynamic languages, well, have never had any problem with them because type safety is not a concern. OCaml does not have either of these so ad-hoc polymorphism is something of a pain point, which may be addressed in future.

The 2ality blog points out an interesting workaround for this by locally overriding the operators by opening a module that aliases the float operators:

So while TypeScript (being a superset) carries over the warts of javascript,  ReasonML also brings in its own set of syntactic eccentricities from its OCaml lineage, some of which may be quite obscure and non-intuitive for frontend developers.

Ecosystem

Many libraries for frontend development are being written in TypeScript and for many more (varying quality of) type definitions are available in the Definitively Typed repository.

There is a similar initiative Reasonably Typed for Reason, but that is not yet as comprehensive as TypeScript.


Summary

This brings us to the end of this post. I hope that this post helps you evaluate the different set of trade offs around which TypeScript and Reason have been designed around and make an informed decision when faced with a choice.

My familiarity with Reason is still very nascent, so please feel free to comment about anything I might have missed out or got wrong.

Key Takeaways

References:

  1. Reason Official Site maintained by Facebook Team
  2. OCaml: Polymorphism by Haifeng Li
  3. Exploring ReasonML by Axel Rauschmayer
  4. Real World Ocaml by Anil Madhavapeddy, Jason Hickey and Yaron Minsky.
  5. Posts on OCaml by Haifeng Li