Typing and code flow in TypeScript and ReasonML
Tags: typescript, reason, ocaml
Published: 2019-03-09 19:42:29

---
title: Typing and code flow in TypeScript and ReasonML
published: true
description: A comparative analysis of the semantics of similar TypeScript and ReasonML code
tags: typescript,reasonml,ocaml
---

WITH the advent of TypeScript as the most popular static typing tool for JavaScript, it's worth taking a look at how TypeScript and ReasonML approach the problems of static typing, inference, and code flow in some typical equivalent code. This is not an apples-to-apples comparison; however, in terms of functionality it is pretty close. My intended audience with this is primarily TypeScript and Reason folks. If you understand one of the languages (up to the basics of generics), you should be able to map between the implementations because of their similarity.

The example I'll use here is a simple mutable map data structure. In TypeScript, the most idiomatic way to build a data structure is using a class. For our map data structure, we can implement it like so:

```typescript
// MapImpl.ts

class MapImpl<A, B> {
  private map: {[key: string]: B};

  constructor() { this.map = {}; }
  get(a: A): B | undefined { return this.map[JSON.stringify(a)]; }
  put(a: A, b: B): MapImpl<A, B> {
    this.map[JSON.stringify(a)] = b;
    return this;
  }
}
```

This map works by stashing values in a JavaScript object, keyed by the JSON serialized string of the given key. Thanks to TypeScript's good support for classes and private members, we get a pretty good data abstraction: trying to access `map` in `MapImpl` objects will be a compile-time error.

## Within Reason

Let's look at the equivalent in Reason (using the BuckleScript compiler to target JavaScript):

```ocaml
/** MapImpl.rei */

type t('a, 'b);

let get: ('a, t('a, 'b)) => option('b);
let make: unit => t('a, 'b);
let put: ('a, 'b, t('a, 'b)) => t('a, 'b);

/* MapImpl.re */

type t('a, 'b) = Js.Dict.t('b);

let make = Js.Dict.empty;
let get(a, t) = a
  |> Js.Json.stringifyAny
  |> Js.Option.andThen((. key) => Js.Dict.get(t, key));
let put(a, b, t) = {
  a
  |> Js.Json.stringifyAny
  |> Js.Option.map((. key) => Js.Dict.set(t, key, b))
  |> ignore;

  t;
};
```

The first thing you'll notice is that this is _two_ files, compared to the TypeScript version's single file. In OCaml (and thus Reason), to make a data type abstract you'll need to hide its implementation details from other code, and the most idiomatic way to do that is to give it an _interface file_ (`.rei`) which just declares the type (`type t('a, 'b)`) but does not define it.

This is the essence of data abstraction in the OCaml world–the type `t` is abstract. We just see that it takes two type parameters `'a` and `'b`, and there are three functions which work with this type. As a side note, we can arrange these types and functions in a different order in the interface file–whichever order best serves our use case. In this case I've ordered the values alphabetically, to make them easier to look up.

## The implementations

In this example, you'll notice that the Reason implementation is more verbose. This is because it's more explicit: operations show in their types that they return 'no result' (`None`), and also show that they use JavaScript objects as a 'dictionary' data structure.

The TypeScript in contrast preserves a lot of the 'JavaScript feel'. Not surprising given that it aims to be strictly a JavaScript superset. However it does lead to the code having a hidden layer of meaning. For example, it's implicit that certain operations could 'fail' and result in `undefined`, and there are automatic rules for how those `undefined` get handled.

Let's take a look at one such code flow: the `MapImpl#put` method. In it, first the `JSON.stringify` operation can fail (silently) and result in `undefined`. Then, this `undefined` can be used as the index key to `this.map`, which will then insert the value with the key `undefined`. This will cause a really subtle bug that you won't even notice until you try to insert two key-value pairs whose keys can't be stringified, and keep getting the wrong value back on lookup. The correct implementation is:

```typescript
put(a: A, b: B): MapImpl<A, B> {
  const key = JSON.stringify(a);

  if (key) this.map[key] = b;
  return this;
}
```

At this point it's worth pointing out that the return type `string` in the TypeScript bindings for the `JSON.stringify` function is incorrect. It should really be `string | undefined`. It can be argued that this is fixable, but I believe that lots of TypeScript typings are similarly incomplete because they were written in a time when TypeScript understood any type `T`, anywhere, to really mean `T | undefined`. Later, the [TypeScript compiler changed](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-0.html#null--and-undefined-aware-types) how it understood type declarations like `T` to mean really just (non-nullable) `T`. So any type `T` from before the change can implicitly mean `T | undefined`, meaning its code may silently fail.

Anyway, the point is that `JSON.stringify` and other typings are still fixable at the library level (one by one, e.g. [issue for `JSON.stringify`](https://github.com/Microsoft/TypeScript/issues/18879)). But the types of things like the indexing operation `this.map[key]` are not (realistically, anyway). If you change the `get` method to:

```typescript
get(a: A): B | undefined {
  const result = this.map[JSON.stringify(a)];
  return result;
}
```

You'll notice that TypeScript infers the type of `result` as `B`. The problem is it's really `B | undefined`, because the key might not be in the object and the lookup might fail. This can't be fixed at the library level–it would have to be a breaking TypeScript compiler change (or at least a new compiler flag). This is the subject of [ongoing discussion](https://github.com/Microsoft/TypeScript/issues/13778).

## The benefit of verbosity

When we think about the semantics and the hidden code flow, it's clear that TypeScript and JavaScript do a lot for us behind the scenes. In a best effort to stay backwards-compatible with JavaScript, TypeScript infers the types of various operations and code paths.

What Reason is trying to do though is actually use a wholly-different semantics–that of OCaml–to make these operations more explicit and better represented in the type system itself, instead of hidden away by the special rules of the operations. The underlying data structures may be JavaScript objects, but we get access to them following OCaml's rules of type safety. It also doesn't hurt that the pipe-forward (`|>`) operator can be used to show the left-to-right, top-to-bottom flow of data inside function bodies.

If you're looking for a way to write JavaScript without dealing with special rules and hidden meanings in your code, ReasonML may be worth a try.