September 3, 2017

Bucklescript vs Elm vs Typescript: Typed Javascript showdown!

You, a web developer, have probably heard of Typescript, may have heard of Elm, and you might even have heard of Bucklescript/ReasonML as well. Each of these represents a compiles-to-javascript language with strong type support, but each has some different opinions, philosophies, and features.

In this article I’ll walk you through the features of each of these options and compare their build ecosystems, editor tooling, and javascript interop. In addition, as is my habit, I’ve written up a small example in each of these languages. (Well, actually, I’ve taken one of Elm’s small demos and re-created it in the other two). I’ll discuss my impressions of the tooling and the coding experience along the way.

Note that of these 3 languages I have only ever used Typescript in production, so take my advice with the appropriate caution.

Typescript

Typescript does its best to be, simply, Javascript with types. For the most part it achieves this. The upside is that you get to use familiar libraries and tooling. The downside is that you’re still writing Javascript, and are therefore fully capable of creating runtime errors (especially with the help of the any type).

I’ve been using Typescript for close to a year now and it’s gotten better with every release. Most notably, the tooling and new configuration options (combined with my learning more about it) has allowed me to get closer and closer to full type coverage, although I’ve yet to find a project that didn’t have to use any to shim some-or-another untyped JS library eventually.

Typescript is supported by Microsoft, which should make it a pretty easy sell to your boss.

Project Creation

To get a project up and running I followed Typescript’s Guide for creating a new project. This involved creating a directory, initializing a package file and installing dependencies with yarn (this was my decision, the guide uses npm), adding a tsconfig.json file, and adding a webpack config file.

While writing this article I actually did Typescript last, and I was mildly annoyed to have to do all that after elm and bsb handled the scaffolding for me, but overall this process was quite easy.

Tooling

I’ve been using VSCode for Typescript projects. It just seems to make the most sense, having first-class support from Microsoft and all, as well as some nice features that make it feel like a mini-IDE.

VSCode’s tooling for Typescript lives up to the hype. Errors are highlighted instantly, autocomplete works quite well, and hovering over symbols with your mouse gives you a pop-up display of that symbol’s type.

There exist several options to integrate typescript with webpack. Of these, Awesome Typescript Loader seems to be the best right now.

Sample Code

import * as React from 'react'
import { render } from 'react-dom'

interface OwnState {
    count: number
}

class App extends React.PureComponent<{}, OwnState> {
    componentWillMount(){
        this.setState({count: 0})
    }

    decrement() {
        this.setState({count: this.state.count - 1})
    }

    increment() {
        this.setState({count: this.state.count + 1})
    }

    render() {
        return <div>
            <button onClick={this.decrement.bind(this)}>-</button>
            <div>{this.state.count}</div>
            <button onClick={this.increment.bind(this)}>+</button>
        </div>
    }
}

render(<App/>, document.getElementById("app"))

If you’re familiar with modern Javascript, this should be very readable. The only type annotations are the interface declaration and subsequent passing of that type to PureComponent. Don’t be fooled, though, this program has type-checking at pretty much every point.

(One mistake you might make if you’re coming from [other language with an interface keyword] is thinking the OwnState interface is or should be reflected in the state of App. In fact, the interface is a sort of record type that can be applied to any Javascript object, and in this case it’s used to tell React[’s typings] what shape the component’s internal state will be taking.)

Javascript Interop

Typescript will almost directly work with Javascript libraries. The only thing you have to do is either a) find or provide type annotations for the library, or b) give up and declare module "some-library";, giving the contents of the library any typing and opening the door to runtime errors.

In practice, you can generally find typings for most popular libraries via the excellent DefinitelyTyped repository. However, the types and the libraries they apply to aren’t developed in lock step, so type annotations can occasionally be obsolete and/or incorrect. Additionally, typescript’s strictness is configurable, so even typescript-first dependencies may not offer perfect typing.

Bucklescript (and ReasonML)

Bucklescript is an OCaml->Javascript transpiler. It mostly works on the OCaml toolchain, and comes with a specialized build tool and configuration file format. Reason is a syntax for OCaml that makes it a bit more… well, Javascript-like. Together, they form a compelling platform for writing strongly-typed-but-still-flexible javascript applications. A notable consequence of this is that, unlike Typescript, Bucklescript code uses OCaml’s types, not Javascript’s (although javascript types are made available).

Bucklescript was developed at Bloomberg, while ReasonML was independently developed at Facebook.

Tooling

Reason is a bit different from either OCaml or Javascript. Among other things, functions are now denoted by => and multi-line function bodies are wrapped with {}. Each statement ends with ;, and the match statement is renamed switch. Records use {name: value} syntax in both types and code.

All of these changes are made much more tolerable by Reason’s first-class Merlin support. Merlin is to OCaml what ts-server is to Typescript – a common solution to editor integration that allows rich support for type information, autocompletion, and general linting. Merlin does require a .merlin file in the project that describes the locations of your code; however, the Bucklescript compiler generates this for you.

In general, I’d but the editor tooling on par with (or even above) Typescript.

In terms of compilation toolchain, the default BSB project is two-stage, requiring that you first compile your reason to javascript, and then use webpack to combine the resulting javascript with the rest of your code. However, you can use the [bs-loader][bs-loader] to have webpack handle all of this.

Sample Code

I’ve implemented the exact same thing as above using ReasonReact, which provides the additional benefit of a JSX-like embedded syntax for generating HTML. Here’s the code.

open ReasonReact;

type state = {count: int};

type action =
  | Increment
  | Decrement;

let inc _event => Increment;
let dec _event => Decrement;

let component = reducerComponent "App";

let make _children => {
  ...component,
  initialState: fun () => {count: 0},
  reducer: fun action state => (switch action {
      | Increment => Update {count: state.count + 1}
      | Decrement => Update {count: state.count - 1}
  }),
  render: fun self => {
    <div>
      <button onClick=(self.reduce dec)> (stringToElement "-") </button>
      <div> (self.state.count |> string_of_int |> stringToElement) </div>
      <button onClick=(self.reduce inc)> (stringToElement "+") </button>
    </div>
  }
};

ReactDOMRe.renderToElementWithId (element @@ make [||]) "app";

If you don’t have existing ML experience, this will probably seem unfamiliar, so I’ll walk you through it:

  • open ReasonReact; imports everything from the ReasonReact module into the local scope. This means I can write things like stringToElement instead of ReasonReact.stringToElement.

  • type state = ... and type action = ... are type declarations that describe the internal state of our component, and the actions it can receive. Later we’ll write a reducer (a la redux) that ties actions to state updates.

  • let inc ... and let dec ... are helper functions that accept an unused _event argument and return an action.

  • let component = reducerComponent "App" defines a “Reducer” component with the name “App”. A reducer component is ReasonReact’s preferred (and only supported) way of defining a component with internal state, which is implemented via a reducer, which accepts and action and a state and returns a ReasonReact.update object describing the new state.

  • let make _children => ... is the constructor function for the component. It extends the component with the methods initialState, reducer, and render.

  • ReactDomRe.renderToElementWithId is the React entry point, roughly equivalent to ReactDOM.render. We have to manually create the element using the cryptic (element @@ make [||]); this first calls the function make with an empty array of _children, then calls ReasonReact.element on the result of that. If I had followed the ReasonReact convention of having my component in a separate file, this whole line could havebeen ReactDOMRe.renderToElementWithId <App/> "app"; instead.

Overall this was quite easy to write, given the existing example from the Bucklescript react template and helped along by Merlin. My biggest complaint is that the JSX port won’t accept strings directly, requiring tedious stringToElement calls in the render function.

Javascript interop

Bucklescript has quite good Javascript interop. Bucklescript functions can be directly called from JS, while external Javascript functions can be annotated as they are imported, via a specialized annotation language.

All in all I get the impression that I wouldn’t want to be heavily depending on Javascript-only libraries in Bucklescript. That said, the collection of libraries written for Bucklescript is growing.

Elm

Elm was developed by Evan Czaplicki (with some help from Prezi). It is heavily Haskell-inspired language/framework hybrid that is purpose-built for single-page Javascript applications.

Tooling and Project Creation

In keeping with its all-in-one-philosophy, Elm comes with a build tool and package manager included – no webpack required (although you could certainly run the compiled source through webpack if you wanted, or use elm-webpack-loader.

Elm’s editor tooling is a bit behind. I didn’t get the VSCode plugin working, but elm-vim managed to take care of live linting, error highlighting, some light completion, and accessible shortcuts to look up docs and make the project from in the editor. The only thing missing is easy access to type annotations.

Gaining a couple of points back for Elm is the excellent reactor tool. Run elm reactor from your project root and you get a browsable file tree, where you can not only view your compiled application but also view your components individually, gaining a nice time-travelling debugger (which works thanks to Elm’s strict adherence to the reducer architecture).

Sample Code

module Main exposing (main)

import Html exposing (Html, beginnerProgram, div, button, text)
import Html.Events exposing (onClick)


type alias Model =
    { count : Int
    }


type Msg
    = Increment
    | Decrement


update : Msg -> Model -> Model
update msg model =
    case msg of
        Increment ->
            { count = model.count + 1 }

        Decrement ->
            { count = model.count - 1 }


view : Model -> Html Msg
view model =
    div []
        [ button [ onClick Decrement ] [ text "-" ]
        , div [] [ text (toString model.count) ]
        , button [ onClick Increment ] [ text "+" ]
        ]


main =
    beginnerProgram { model = { count = 0 }, view = view, update = update }

To the unfamiliar, Elm is even more alien than ReasonReact, owing in part to the fact that Elm is both a language and a frontend development framework. Architecurally, though, this code is actually quite similar to the ReasonREact implementation, if you just replace state with Model and action with Msg.

  • module Main exposing (main) establishes this as the Main module and exposes the main function (the entry point to your app)

  • type alias Model is just like in Reason, a typed record with a single count field.

  • type Msg ... enumerates the possible Messages that can be sent from within this component.

  • update is just like reduce from before – it accepts a message and a model, and returns an updated model.

  • view is like render from before – it accepts the model (i.e. the current state) and generates HTML from that. It does this using a special DSL (each element is of the form <tag> <attributes> <content>)

  • main is exported, generating an Elm Program that the compiler will embed in the output.

JS Interop

Elm has an interesting strategy for interop. Rather than calling JS functions from Elm and/or vice-versa, Elm allows you to write code that exposes “ports”, which are the only places where JS and Elm are allowed to interact. Ports may send messages conforming to JSON’s datatypes, and the messages are typed-checked at the border into Elm – values not conforming to the type of the port’s receiver will instantly throw.

This system is the safest of all the above options, but the most restrictive, too. Interacting with Javascript libraries would necessarily require that bespoke Javascript adapters be written. The end result of this seems to be that Elm encourages users to use Elm libraries exclusively. Luckily, there are many of these available.

Comparison (i.e. TL;DR)

  • Typescript

    • Pros
      • Microsoft-supported (probably isn’t going away)
      • Widest support/community/docs
      • Familiar Syntax
      • Excellent tooling
      • Best integration with larger JS ecosystem
    • Cons
      • Less-than-perfect typing
      • Javascript-like syntax
      • No added support for immutable data structures or functional programming
  • Bucklescript + ReasonML

    • Pros
      • Significant support from Facebook and Bloomberg
      • Complete type system
      • Utilizes existing OCaml tooling
      • Excellent editor support
      • Easy to call from Javascript
    • Cons
      • Somewhat limited Javascript FFI (for calling JS libraries from Reason)
      • Unfamiliar syntax
      • Slightly awkward JSX syntax
  • Elm

    • Pros
      • All-in-one JS single-page-app support
      • Best type system of the lot
      • Safest JS interop
      • Reactor
    • Cons
      • Less-than-perfect editor support
      • HTML DSL is a bit weird, innit?
      • Most complex JS interop

Of the above, I think I’m most interested in exploring Bucklescript. Typescript doesn’t offer complete type-safety, and I’ve found Elm a bit restrictive in the past.

Other libraries

I didn’t have time to try every single option, so here’s a few I missed:

  • Flow is Facebook’s official static JS language. It’s a lot like Typescript as far as I can tell.
  • js_of_ocaml is another ocaml-to-javascript compiler. This one focuses heavily on OCaml support, and, unlike Bucklescript, supports almost all of OCaml’s standard library!
  • Purescript is another Haskell-like compiles-to-Javascript system, with less emphasis on being a single-page-app framework and more on being a Haskell. It has typeclasses!