At Hasura, we wanted to introduce a statically typed frontend language for quite some time now. We discussed which one we should choose and evaluated options such as PureScript, TypeScript, ReasonML, and Elm. Following aspects were most crucial to us:

Hasura Console is a pretty large codebase for a small team, so we can't rewrite it all to the new language. We need something that works well with the existing JavaScript code. A language that we can inject into our codebase and gradually migrate.

We use React extensively. Thus we need something that goes with React well and improves React components development.

We want to enhance developer experience for the external contributors and us. By adopting a statically typed programming language, we aim to make things easier, and the development process way faster. We don't want to introduce high adoption cost nor force developers to learn a new and completely different language.

After all of the discussions, we decided to choose TypeScript. In this article, I'm going to tell you how we made the decision and why we wanted a statically typed language in the first place. I will also walk you through all of the four languages by a brief overview of each of them.

Why do we want static typing?

Introducing a new language always comes with a cost. Developers need to invest time into setup, integration and then adoption. The team may also be less productive for a while until everyone becomes fluent in a new language. All of this will take a more or less significant amount of time depending on the chosen language, but it always does take time.

That's why every team needs to consider all the benefits of the statically typed language adoption, decide whether they outgrow the cost and think through why they need a new language. Answers to this question may be different among the teams and among the particular developers as well. For us, the following reasons were most important:

A lot of errors may be caught in an early phase of development and be fixed immediately, rather than lurking in the code to be discovered much later.

Refactoring becomes more straightforward with the statically typed language. Developers gain more confidence thanks to the compiler or type-checker, notifying them about the type errors.

Types serve as excellent documentation. It's easier for new developers to dive into some fragments of the codebase and start working with them without any broader knowledge about the codebase.

Safety that comes with static typing can be a huge productivity boost.

PureScript

PureScript has been around since 2013 and is maintained by the community. Inspired by Haskell and ML, it is a purely functional programming language that provides many features to help with code correctness. Some of these features are immutability, pattern matching, strong type inference, and a powerful type system. It also has a great FFI (Foreign-Function Interface) that allows us to call JavaScript functions from within PureScript.

Why PureScript?

PureScript has a decent ecosystem. There are bindings for many JavaScript libraries, and even when there's no support for some library, PureScript FFI makes it easy to include them yourself.

PureScript has a great type system and provides features such as typeclasses, higher kinded types, row polymorphism, higher-rank types, and many more.

It's a purely functional language, so if you're a fan of functional programming, PureScript is a great choice. The style of programming in PureScript empowers you to maximize functional purity, strictly limiting state and side effects.

Main PureScript target is JavaScript but it can compile to other languages as well. You can write full-stack applications in PureScript.

How to setup PureScript in the Console codebase?

1. Install all the required dependencies and initialise new PureScript project with spago:

yarn global add purescript spago yarn add -D purs-loader spago init spago install purescript-react-basic

spago init command will create a new files:

packages.dhall: this file is meant to contain the totality of the packages available to your project.

spago.dhall: project configuration — among others the list of your dependencies, the source paths that will be used to build.

2. Update webpack configuration by adding loader for the PureScript files and handling .purs extension.

Adding purs-loader to the webpack config

Handling .purs extension

3. Now, we're ready to start writing code in PureScript! Below is the example of a simple button component written in PureScript:

Button component in PureScript

Elm

Elm is a purely functional programming language designed in 2012. Elm uses abstractions called flags, ports, and custom elements to communicate with JavaScript. The Elm Architecture pattern makes it easy to develop frontend applications. The three concepts that are the core of The Elm Architecture:

Model — the state of your app,

— the state of your app, View — a function to turn your state into HTML,

— a function to turn your state into HTML, Update — a way to update your state based on messages.

The current implementation of the Elm compiler targets HTML, CSS, and JavaScript.

Why Elm?

Elm has a strong type system and great type inference.

It promises no runtime exceptions. It uses type inference to detect corner cases and world-class compiler messages help a user with debugging.

Elm has great performance. Comparing it to React and Vue it seems to produce slightly-smaller bundle sizes and faster render times.

Beginner-friendly syntax makes it easy and fun to use. At the same time, it's a very powerful language that embraces all the good parts of functional programming.

How to setup Elm in the Console codebase?

1. Install dependencies. react-elm-components allows to use Elm components inside React.

yarn add -D react-elm-components elm-webpack-loader

2. We also need to add elm.json file with Elm project configuration.

3. Update webpack configuration.

4. Example component in Elm:

TypeScript

Typescript is a typed superset of JavaScript developed and maintained by Microsoft. It adds optional static typing to the JavaScript world and its adoption can bring you more robust software at super low cost. Since it's a superset then any valid JavaScript is a valid TypeScript, so basically you can just change the extension from .js to .ts and, et voila, you have a valid TypeScipt file. From there, you can incrementally add type checking where you think it’s necessary. It's important to notice that it's not a completely new langauge — it's just JavaScript with additional features, and most JavaScript pitfalls.

TypeScript transpiles to JavaScript with help of TypeScript Compiler (tsc) written in TypeScript.

Why TypeScript?

TypeScript brings optional static typing, meaning that you can write type annotations, but you don't have to. Whatever you feel like. It also makes it easier for JavaScript developers to dive into TS.

TypeScript has high compatibility with JavaScript, meaning that every JS library is going to work in TypeScript code and vice versa.

There are many ways to adopt TypeScript — you can add type checking with // @ts-check or write declaration files (d.ts) to have TypeScript benefits without writing TypeScript.

TypeScript is designed with gradual adoption in mind.

Zero configuration support in many modern IDEs. For example VS Code or WebStorm have TypeScript support working out of the box.

How?

1. Install dependencies:

yarn add -D typescript @babel/preset-typescript fork-ts-checker-webpack-plugin

2. Update .babelrc file.

3. Update webpack config.

4. Example component in TypeScript:

ReasonML

ReasonML is a syntax extension for OCaml — the statically typed functional language with object-oriented features developed in the late 1990s. Reason was created by Facebook and provides the same features as OCaml does, but its syntax is more similar to JavaScript. The intention behind this is to make adoption by JavaScript programmers easier.

Reason doesn't directly compile to JavaScript. .re files are transformed into the OCaml AST by OCaml preprocessor for Reason (refmt). It is then processed by the BuckleScript compiler called bsc, which produces JavaScript files.

Why ReasonML?

Reason has a rock solid type system and strong type inference.

Reason is immutable and functional by default, but it supports mutations and side-effects.

The syntax is similar to JavaScript.

Reason supports React with ReasonReact and JSX syntax. In fact, first prototypes of React were done in SML — another dialect of ML. Also, React and Reason share the same creator.

JavaScript package managers work with Reason out of the box. You still can use npm and yarn.

How?

1. Install dependencies:

npm install --save-dev bs-platform reason-react

2. Add bsconfig.json:

3. Update scripts:

4. Example component:

Comparison

Dynamically typed languages are great for prototyping; they can give us a lot of flexibility, which results in significant development speed. Statically typed languages, on the other hand, provide more control, increase program correctness, but they also may decrease the speed of adding new code.

However, they make working with the existing code easier. That's why we need to decide what can make us most productive.

We need to determine where we want to be in the diagram below. What is more important for us? Development speed or correctness and control?

Development speed vs Control

The languages we were discussing are different from each other in many regards. We can't say that one is superior and the other is significantly worse. What we can do is compare them with the aspects essential for us and our project.

JavaScript interoperability

TypeScript is a superset of JavaScript, so it works almost out of the box with JavaScript. You can call the JS code from the TS file and vice versa. The only thing that you need to do is to find or provide type definitions for the JavaScript modules.

ReasonML and PureScript have pretty similar JavaScript interop. Both BuckelScript and PureScript compilers produce a readable and performant JavaScript code that you can use in any JavaScript file. If you want to call external JavaScript functions in ReasonML or PureScript code, you need to provide type annotations for them. They also both require that the boundary between them and the existing JavaScript code is explicitly defined.

Elm provides the ability to interoperate with JavaScript through ports and web components, which are deliberately quite limited, which leaves Elm behind its competitors when it comes to JavaScript interop experience.

IDE support

From my point of view as a VSCode user and my experience with those languages, TypeScript is a winner here. VSCode is written in TypeScript, and it has first-class support for this language. There are plugins for various editors available for all of these languages, but only one of those languages has builtin support in popular opensource IDE. In my opinion, an IDE purpose-built for a language will always provide a better developer experience (see RubyMine, PyCharm, etc.).

Type safety

Languages that we were discussing fall into two groups. TypeScript is a gradually typed programming language, which means its type system allows both statically typed and dynamically typed expressions. As the name suggests, it will enable us to introduce static typing to the existing dynamically typed codebase gradually. TypeScript's type system is unsound, which means that there's no guarantee that static type predictions are accurate at runtime. Here are some examples of type unsoundness in TypeScript:

const push3 = (arr: Array<string | number>): void => { arr.push(3); } const strings: Array<string> = ['foo', 'bar']; push3(strings); const s = strings[2]; console.log(s.toLowerCase()) // runtime error

const cat = dog as Cat; // runtime error

type Foo = { bar?: { x: number; } } const foo: Foo = {}; const x = foo.bar!.x; // runtime error

TypeScript gives you tools to work around the type system, so unless you're careful, you can't always trust it to have your back. Type soundness is one of the most significant advantages of having a proper static type system as we have in TypeScript alternatives.

PureScript, Elm, and ReasonML are in the ML family, so they come with a sound and robust type system. If the program is well-typed, then the type system ensures that it's free from certain misbehaviours. They are entirely different programming languages that support JavaScript as a compile target, and as a consequence migration from JavaScript code requires more effort as in case of TypeScript.

Summary

After evaluating all the possibilities, we decided to go with TypeScript. We think that this choice would improve our productivity with minimal adoption cost. Yes, this language has fewer features than its alternatives, and notably, it brings less type safety. Type inference is much weaker, and it's still JavaScript, which means it brings most of the JavaScripts snares with it. Yet, despite its drawbacks, TypeScript satisfies our needs — with minimal effort, we can improve our codebase. Both setup cost and time for onboarding developers are small, and it doesn't introduce a lot of new things to the codebase. It's just our old JavaScript with types.

We're going to adopt TypeScript slowly, the new things we will write in TypeScript and the existing JavaScript code will be gradually migrated. Do you want to know how it went? Stay tuned! We hope to publish How did we adopt TypeScript in the Console piece soon!

