TypeScript vs. ReScript vs. F# - a simple comparison of syntax
Tags: typescript, rescript, fsharp, programming
Published: 2022-02-15 12:52:00

How could you get the nice `obj.add(obj2).add(obj2)` chaining from OOP, without the downside of unrestricted mutability?

Let's compare how 3 languages that compile to JavaScript - [TypeScript](https://www.typescriptlang.org/), [ReScript](https://rescript-lang.org), and [F#](https://fsharp.org/) with the [Fable](https://fable.io/) compiler - are able to chain immutable data (aka. create an [Immutable Fluent Interface](https://en.wikipedia.org/wiki/Fluent_interface#Immutability) aka. a kind of Fluent API).

We'll use the simplest appropriate data structure we can think of: a Rectangle with 2 properties (height and width). Let's make some Rectangles, and add their properties together, so the resulting Rectangle is a square.

(Huge thanks to @texastoland for coming up with most of these examples and aiding in refining them.)

**TypeScript (plain)** - [try/edit code online](https://www.typescriptlang.org/play?useDefineForClassFields=true&target=99#code/MYGwhgzhAEBKCmwAuYB2BzE9oG8BQ00ATvGACYD2qIAntABbwCW69SAXNKgK4C2ARvCIFipStToB3JmST1OPAUJHAqEJEW7IKRABSMWbBX0FEANNGmz5XE0ICUuEYUJymEAHQHWSaAF4GZh9nFzdPKzl-Sxk5EQBfQhFyMl0SYE4EZDRMeEd8FxcSJG4iVC54SThEFAwsXTCvILYAajTGwyQLBoj6VsQPHvt46Dw4vDxVVHVoFBAQTN8A1AqqrNr4XQBGCwBmIcnpq3gFqOXKhey6nYsAJiG8AHoH6ABVCDB0eAtgejAmVH+6HYEzUvggAEduGASFFZvNqh5krojgt7IiyCkUdV7gcKFgPCAKOhdBCoSQhmkdu0fFEbgAGaBPaAAFQAytBVEQ0khaNAhEQdDAqDN6O4PNAABIUSTwABuQi6jGgACl2ZIKNwQGRoOomHM+QAPRDcJDYZGMMqaMr-PkedDithIAAOEHYT0kHo8SBoTvgEGARCYTp52Q8OnQDyd4Do+zUePgBKJJMh0NyeCAA)

```ts
const square = tallRect.add(wideRect).add(wideRect)
```

```ts
class Rectangle {
  readonly height: number
  readonly width: number
  constructor(height: number, width: number) {
      this.height = height
      this.width = width
  }  
  add(rec: Rectangle) {
      return new Rectangle(this.height+rec.height, this.width+rec.width)
  } 
}

const tallRect = new Rectangle(1, 3)
const wideRect = new Rectangle(3, 2)

// Usage, chaining:
const square = tallRect.add(wideRect).add(wideRect)

console.log(square) // Rectangle: { "height": 7, "width": 7 }
square.height = 20 // TS correctly errors on this. However, the JS would still execute (when run in e.g. https://www.typescriptlang.org/play )
console.log(square)
```

We can make the TypeScript version slightly more terse. If you come from FP you might dislike `new` keyword, so instead of a normal object constructor we use a function `make` instead (a convention from ReScript and OCaml). `Rect.make` better signifies that it is an immutable object, since with the `new` keyword the next programmer could think she got a normal object she'd be able to mutate as any other.

**TypeScript (terser)** - [try/edit code online](https://www.typescriptlang.org/play?useDefineForClassFields=true&target=99#code/MYGwhgzhAEBKCmwAu0DeAoa1gHsB2ESATgK7I5EAUR8YAJviAJ7QDuAlnUgBYBc0eEgFsARvCIAaaDXqMW3eOwDm3JP0GjxASjQBfTNEJgk7YNNoM8zaELABreNAC80Sq3XCxk6HwGftzgB8AvCs0DzsEG5S3FoGMpbW9HTOrqhsnDwxiioouvwIyDpOgQZYhUgAdLYOlBEQlRxc3NAA1BnNUvWVCsqqbT45qnH66LgEKEhgICAVqRXV9vCUAIxSAMxx44Qd8HMuCzXL61IATHHoAPSX0ACqEGBK8FLA3GDseB9KvGP4OxAARxIYBoqSmMwWyTcnD2iCQWkqUKasKK6F+BBwIHglRAOCUlEBwJocUJIOxvVyqVOAAZoNcMjNoOIiBQpBAPsBHOwkAByGDsIRCEhTERYqTKPAURwiFgABxosvgeDoXwyPDpNwAAkgIABaeAAD0VyH1RBZRHREEx2Nx+NJxKAA)

```ts
const square = tallRect.add(wideRect).add(wideRect)
```

```ts
class Rect {
  constructor(readonly width: number, readonly height: number) {}
  static readonly make = (w: number, h: number) => new this(w, h)
  readonly add = ({ width, height }: Rect) =>
    Rect.make(this.width + width, this.height + height)
}

const tallRect = Rect.make(1, 3)
const wideRect = Rect.make(3, 2)

// Usage, chaining:
const square = tallRect.add(wideRect).add(wideRect)

console.log(square) // Rectangle: { "height": 7, "width": 7 }
square.height = 20 // will error, since it's immutable, ignore by prepending with // @ts-expect-error
console.log(square)
```

**ReScript** - [try/edit code online](https://rescript-lang.org/try?code=C4TwDgpgBAThDGwoF4oG8BQUoHcCWAJsABYBcUAZgDYD2AhsADRZTER4Dmxw519TGAL4YMVCEjoECKKAAoYARkawATAEoUAPnQt8RMrAUA6PSSgBqI6pOESygPT2oJaJah4AzpRowokgngAdhyUtAweLGyc3OSKRlFcSG4wKvHsiczCouLOdFRUAEoISKhopgbGygkxUADMRsJiSHoQRYgyZbYG9VXpNalZjlAAqh50HBDK8MR0QUEcpNlIHgCOAK50cDLAeYXFALSa-rItbcBqh8enxWoiAFIeRrQcsqsbcLdvmxBp0SVQKgADFYhvh8lAIDAYD5lB4gvBoHhgAByLx4AC26LWOwARmIMA8njQXl8PkA)

```rescript
let square = tallRect->add(wideRect)->add(wideRect)
```

```rescript
type rect = {
  width: float,
  height: float,
}

let add = (r1, r2) => {
  width: r1.width +. r2.width, // the +. is for adding floats
  height: r1.height +. r2.height,
}

let tallRect = {width: 1., height: 3.}
let wideRect = {width: 3., height: 2.}

// Usage, chaining:
let square = tallRect->add(wideRect)->add(wideRect)

Js.log(square) // { "height": 7, "width": 7 } // The ReScript playground will output JS that can be run in JSFiddle: https://jsfiddle.net/8a3evo4u/
square.height = 20 // will error, since it's immutable
Js.log(square) // { "height": 7, "width": 7 } // The ReScript playground will output JS that can be run in JSFiddle: https://jsfiddle.net/8a3evo4u/
```

**F# and Fable** - [try/edit code online](https://fable.io/repl/#?code=C4TwDgpgBAShDGwoF4BQUoG90agdQEsATYACwC4oAzAGwHsBDYHDACQgIHNThLbHmGAL44AthFEAjCACcoMgIwA6BkSIAKGQCYAlChZYDGQiVIZk85SbJQA1PK1LrpANxGo7LjxSWln7kj22n4cAW64UCIYAM7ATATwUOJSslDqtjqaCpRwiAA0DjkIwHoWiipqmrpQAPQ11HRy0SAAdnEAHlDRAK6cDDKoqDQQSHE0NLlIFpj4xDYWCi4eod4WAMyRQyNQAO7EEJM+M84+a0v+q1Bam6h1UACq0QycEAXwpAwELV+c5FtI0QAjt1+tALGMJsUKho9kQDsUdND1LD4YgdP8usDQddwQxxod7CiCbt9oc7t1otBmm0GJ0en0BqgwDIvsAqC0oAAiACkAHlOZiQTIIKggUKICEvEgADwAWiuAAZavU9uMoLIZI0CtEvvBoARgAByaJQAiiUTdOKSYairHCxwXGXyrRKu6qmjqmSamTa3X6o0ms0Wq025ms9lcvkCsWgoA&html=DwCwLgtgNgfAsAKAAQqaApgQwCb2ag4CdMTJcMABwFp0BHAVwEsA3AXgCIBhAewDsw6AdQAqAT0roOSAMb9BAzoIAeYAPThoAbhkhMAJwDOJNgzAAzagA4OeQhqy5EhAEY9sYu6mBq3HvD6asEA&css=Q)

```fsharp
let square = tallRect + wideRect + wideRect
```

```fsharp
type Rect =
  {
    Width: float
    Height: float
  }
  // here we use a type extension, NB: https://fsharpforfunandprofit.com/posts/type-extensions/
  member r1.add(r2) =
    {
      Width  = r1.Width + r2.Width;
      Height = r1.Height + r2.Height;
    }
  static member (+)(r1: Rect, r2: Rect) = r1.add(r2) // enables syntax sugar

let tallRect = { Width = 1; Height = 3 }
let wideRect = { Width = 3; Height = 2 }

// Usage, chaining:
let square = tallRect.add(wideRect).add(wideRect)
let square2 = tallRect + wideRect + wideRect // uses syntax sugar

printfn "%O" square // { Width = 7 Height = 7 }
square.Height <- 20 // will error, since it's immutable
square2.Height <- 20 // will error, since it's immutable
printfn "%O" square // { Width = 7 Height = 7 }
```


Notice how in the ReScript and F# versions you don't have to make properties immutable with something like `readonly`. You also don't have to annotate types apart from the declaration, whereas in each TypeScript version you have to help the type system by annotating types (at least) at every function boundary, to say if it takes in a `number` or a `Rect` (such annotations can quickly add up and become boilerplate and noise).

That's because both ReScript and F# were derived from [OCaml](https://ocaml.org/), so they also have the powerful [Hindley-Milner](https://en.wikipedia.org/wiki/Hindley%E2%80%93Milner_type_system) (H-M) type inference. H-M type inference is also [sound](https://stackoverflow.com/a/21437375/380607), which means you can rely on it (it prevents all type errors it claims to prevent, and doesn't give false positives, so you can trust that [all type checked programs will be correct](http://logan.tw/posts/2014/11/12/soundness-and-completeness-of-the-type-system/)). That's something you can't take for granted in TypeScript, even with the extra annotations.