02 Feb 2022

Let's write Fable bindings for a JS library

Writing fable bindings is easier than many people think it is, but still require a time investment depending on how much of the API of a Javascript/Typescript library you want to expose. Having a Typescript definitions file will give you a very big advantage as you can see exactly what types a function expects and returns, without implementation details getting in the way. Additionally, there is the ts2fable project, which attempts to automagically type out everything in the definitions file. This is usually a good starting point. The official fable documentation is a good resource, and one of the resources I used to learn how to write bindings.

General tips

Because Javascript is a dynamically typed language, and F# is statically typed, there will be times at runtime that the types defined in F# won't match exactly what is expected. This is most problematic when dealing with data coming from 3rd party services or other libraries, that can return data in different formats depending on different inputs. We will explore some techniques to help mitigate this below. Another thing to keep in mind is that an F# record type will be transpiled into a JS class with support for equality checking. That means a certain amount of code generation that you might not want. In contrast, you could use an anonymous record that will get completely erased at compile time.

Importing your library

Javascript has many ways to import libraries and their functions. Here's some techniques to help you import what you want.

Give this Typescript code:

// examples.ts
export const answerToLife = 42;
export const add = (a, b) => a + b;
export default {
    answer: answerToLife,
    add: add
}


// using example.ts

import * as everything from './exporter.mjs';
import { add } from './exporter.mjs';
import Things from './exporter.mjs';

console.log(everything.answerToLife)
console.log(add(1, 2));
console.log(Things.answer)

We would write the following F# code:

open Fable.Core.JsInterop

let answerToLife: int = import "answerToLife" "./examples.ts"

let add (x: int) (y:int): int = import "add" "./examples.ts"

type everythingType =
    { answerToLife: int
      add: int * int -> int }
let everything: everythingType = importAll "./examples.ts"


type defaultImportType =
    { answer: int
      add: int * int -> int }

let defaultImport: defaultImportType = importDefault "./examples.ts"

Binding simple functions

Individual JS functions are rather straightforward, as they're static.

export const eventualAnswer = new Promise(() => { return 42 });
export const split = (stringToSplit, splitter) => stringToSplit.split(splitter);

F#:

open Fable.Core.JS
open Fable.Core.JsInterop

let eventualAnswer: Promise<int> = import "eventualAnswer" "./examples.ts"

let split (stringToSplit: string) (splitter: string): string array = import "split" "./examples.ts"

Javascript/Typescript classes

When it comes to classes, our bindings will require several steps. Most notable, a function that is the actual import, an abstract class which holds static members, and another abstract class which holds everything else.

Given this TS class:

class MyClass {
    get number() {
        return this._number;
    }
    set number(value) {
        this._number = value
    }

    constructor(private _number) {
        console.log(`constructing with param: ${_number}`);
    }

    public multiplyMyNum(multiplier: number) {
        return this._number * multiplier;
    }

    static multiplyTwoNumbers(num1: number, num2: number) {
        console.log(`multiplying ${num1} with ${num2}`)
        return num1 * num2;
    }
}

export default MyClass;

We need to define several things in F#:

open Fable.Core
open Fable.Core.JsInterop

type MyClass =
    abstract number: int with get, set

    abstract multiplyMyNum: multiplier: int -> int


type IMyClass =
    [<Emit("new $0($1)")>]
    abstract Create: int -> MyClass
    abstract multiplyTwoNumbers: num1: int * num2: int -> int

module Account =
    let myClass: IMyClass = importDefault "./examples.ts"

// Example usage
let answer = Account.myClass.multiplyTwoNumbers(2, 5)
let myInstance = Account.myClass.Create(4)
let newValue = myInstance.multiplyMyNum(5)
let currentInstanceNumber = myInstance.number
myInstance.number <- 42

Becomes:

examples from "./examples.ts";

const Account_myClass = examples;

const answer = Account_myClass.multiplyTwoNumbers(2, 5);

const myInstance = new Account_myClass(4);

const newValue = myInstance.multiplyMyNum(5);

const currentInstanceNumber = myInstance.number;

myInstance.number = 42;

Here we see a new attribute Emit. Essentially it emits that exact JS code with replacements.

Emit Javascript code directly

Sometimes writing a binding will require outputting some JS code that doesn't directly map to what the import functionality can offer. This is where the Emit attribute comes in. It tells the Fable compiler to emit this exact Javascript code. Additionally, there is fancy syntax for parameter replacements. Let's take a look at some examples:

[<Emit("[...$0,...$1]")>]
let combineArrays (numberArray: int array) (stringArray: string array): obj array = jsNative

let newArray = combineArrays [|1;2;3|] [|"fef"; "greger"|]

Becomes:

const newArray = [...(new Int32Array([1, 2, 3])),...["fef", "greger"]];

The parameter replacements can get incredibly fancy, and the official docs to a decent job of showcasing it. However I still want to highlight the special case of $0 and how it applies to classes and class instances. As we saw in the class example above, the function IMyClass.Create emits some code calling the new keyword and then $0. In this case, the parameter refers to the class definition.

Additionally, if we need to Emit a class member, we need to reference the created class instance. As an example, if we add the following method to our class above:

type MyClass =
    [<Emit("$0.myMethod($1, $2)")>]
    abstract something: paramOne: string * paramTwo: bool -> obj array

We need to call myMethod on the constructed instance, but we don't know what Fable will call the constructed class instance. This is where the special meaning of $0 comes into play, and Fable knows to replace it with the class instance instead of the first parameter. Usage of it will end up looking like this:

let myInstance = Account.myClass.Create(4)

let newArray = myInstance.something ("wefwe", true)

Becomes:

const myInstance = new Account_myClass(4);

const newArray = myInstance.myMethod("wefwe", true);

Functions that can accept different types

Since Javascript is a dynamically typed language, there are times when a function will accept variety of types for a given parameter and then act differently on them. In Typescript this is handled with something called Sum Types, which are similar to F# discriminated unions. When you come across such functions, a Typescript definitions file is incredibly useful. There are several techniques you can use to write your bindings for it. Fable comes with special types and operators to handle these cases, or you can defined some overloads.

Given this Typescript function:

export declare class Collection {
    setContent(content: obj | string): Promise<void>;
}

We can either use the special Fable types like this:

open Fable.Core
open Fable.Core.JsInterop
open Fable.Core.JS

type Collection =
    abstract setContent: content: U2<bool, string> -> Promise<unit>


// Example usage
collection.setContent (U2.Case1 true)

// or
collection.setContent (U2.Case2 "content")

// or special fable operators
collection.setContent !^"content"

Using this syntax will, the parameters will be type checked, including the operator. However someone unfamiliar with this syntax might get confused, or the consumer of the bindings library might get unwieldy and difficult to read. An alternative is to use method overloads:

type Collection =
    abstract setContent: content: string -> Promise<unit>
    abstract setContent: content: bool -> Promise<unit>


// Example usage
collection.setContent true

// or
collection.setContent "content"

Here the syntax is much cleaner and easier to read. So you can choose whichever version you prefer. My personal opinion is to use method overloads, however if the JS library has a function with multiple parameters that each can accept several different types, you will end up with a large amount of overloads. In these scenarios, using the special Fable types might be advantageous.

Closing words

I hope you were able to learn something from this post, and between this post as well as the official docs, you can write your own Fable bindings for any JS/TS library. I myself wrote my first Fable bindings for the etebase library, which has a relatively small API, and also comes with a Typescript definitions file. So that might also be a good resource for seeing various examples. View the bindings on Codeberg here, or Github here.