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:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: |
|
We would write the following F# code:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: |
|
Binding simple functions
Individual JS functions are rather straightforward, as they're static.
1: 2: |
|
F#:
1: 2: 3: 4: 5: 6: |
|
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:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: |
|
We need to define several things in F#:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: 17: 18: 19: 20: 21: 22: 23: |
|
Becomes:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: |
|
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:
1: 2: 3: 4: |
|
Becomes:
1:
|
|
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:
1: 2: 3: |
|
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:
1: 2: 3: |
|
Becomes:
1: 2: 3: |
|
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:
1: 2: 3: |
|
We can either use the special Fable types like this:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: 11: 12: 13: 14: 15: 16: |
|
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:
1: 2: 3: 4: 5: 6: 7: 8: 9: 10: |
|
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.