Calling Fable from TypeScript
Fable is a framework used to compile F# code to JavaScript. I’ve been using it in a TypeScript project — actually going so far as using F# for most of the code.
Fable has a really nice interface that allows easy interoperability with JavaScript, although it can take some time to fully grasp how to use the interface.
Unfortunately this interface isn’t really well documented. They do have some introductory instructions there, but nothing really comprehensive.
While using Fable in my project I had a really hard time figuring out how to call the compiled F# code from my TypeScript project, even going so far as converting the project to JavaScript while I was figuring this out because I really wanted to use F# instead of TypeScript.
So, without further ado, let me explain how you go about importing your compiled F# code into your TypeScript project!
My interpretation of the documentation
After reading the documentation I thought I should import the Fable code into
index.ts
with an import command like
import { helloWorld } from 'fsharp/helloWorld.fs'
and a corresponding
declaration file, but that doesn’t really work; you can’t import F# code directly into TypeScript, or any
non-TypeScript code for that matter.
To import non-TypeScript code into your TypeScript project, you need a declaration file, but before getting to that I want to talk about project folder structure. This is because the declaration file itself isn’t enough, it has to be in the right location.
Project folder structure
Lets assume your project folder looks something like this:
project/
|- src/
|- index.ts
|- fsharp/
|- helloWorld.fs
|- helloWorld.fsproj
And let’s assume you’ve set up your compilers such that the compiled output looks like this:
project/
|- out/
|- index.js
|- fsharp/
|- helloWorld.js
To be able to import your F# code into your TypeScript project, you need to think about the output folder structure, and subsequently where you should put your declaration files.
You always need to think about the end result; the compiled code will always be pure JavaScript, and thus you need to think as if you’re working in JavaScript.
Assume for a moment that you’re the one writing the code in the compiled output
out/index.js
. If you wanted to use the code from
out/fsharp/helloWorld.js
you’d import it with
import { helloWorld } from 'fsharp/helloWorld.js'
. Because of this, we need the
import in src/index.ts
to be that exact import.
The easiest way to achieve this is to have the source directory mirror the structure of the output directory. Take a look at the example I outlined above again, here, in its entirety:
project/
|- out/
|- index.js
|- fsharp/
|- helloWorld.js
|- src/
|- index.ts
|- fsharp/
|- helloWorld.fs
|- helloWorld.fsproj
You’ll see that the src/
and out/
directories look the same, aside
from the F# specific helloWorld.fsproj
.
Where to put your declaration files
With the mirrored folder structure in mind, we put the declaration file somewhere that will
make the import from src/index.ts
work for the compiled version of the
JavaScript. In this case that would be in src/fsharp
.
We also need to make sure that the name of the declaration file fits the import. Remember
that the compiled F# code will live in out/fsharp/helloWorld.js
. For this
reason we’ll call the declaration file helloWorld.js.d.ts
and save it in
src/fsharp
:
project/
|- out/
|- index.js
|- fsharp/
|- helloWorld.js
|- src/
|- index.ts
|- fsharp/
|- helloWorld.js.d.ts
|- helloWorld.fs
|- helloWorld.fsproj
You can think of the declaration file name as <import_path>.js.d.ts
.
What should you put in your declaration file
This is another part where the documentation fails us. Lets assume your
src/fsharp/helloWorld.fs
file looks like this:
// helloWorld.fs
module Hello
let helloWorld (name: string): string =
sprintf "Hello, %s!" name
Based on the documentation, the declaration file for this
helloWorld.fs
should look something like this:
/** helloWorld.js.d.ts */
declare module "helloWorld.js" {
function helloWorld( name: string ): string;
}
However, I found that this didn’t work for me at all. Nothing. Nada. The
TypeScript compiler simply told me that helloWorld.js is not a module
.
Instead, you’ll want your declaration file to look like this:
/** helloWorld.js.d.ts */
export function helloWorld( name: string );
Simple, and to the point. Just export your function signature, and be done with it.
If your code is a little more substantial, you have to change the declaration file accordingly:
// helloWorld.fs
type HelloMessage =
{ name: string
message: string }
let helloWorld (msg: HelloMessage): string =
sprintf
"Hello %s! Your message is: %s"
msg.name
msg.message
/** helloWorld.js.d.ts */
export class HelloMessage {
name: string;
message: string;
}
export function helloWorld( msg: HelloMessage ): string;
In conclusion
- Mirror the compiled JavaScript folder hierarchy in your source folder.
- Put your declaration files in the same folder as your F# source files.
- Name your declaration files
<f#_source_name>.js.d.ts
. -
Import in TypeScript with
import { yourFunction } from 'path/to/fsharp/source.js'
.