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 likestringToElement
instead ofReasonReact.stringToElement
.type state = ...
andtype action = ...
are type declarations that describe the internal state of our component, and the actions it can receive. Later we’ll write areducer
(a la redux) that ties actions to state updates.let inc ...
andlet 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 areducer
, 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 methodsinitialState
,reducer
, andrender
.ReactDomRe.renderToElementWithId
is the React entry point, roughly equivalent toReactDOM.render
. We have to manually create the element using the cryptic(element @@ make [||])
; this first calls the functionmake
with an empty array of_children
, then callsReasonReact.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 havebeenReactDOMRe.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 themain
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 likereduce
from before – it accepts a message and a model, and returns an updated model.view
is likerender
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 ElmProgram
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
- Pros
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
- Pros
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
- Pros
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!