Kristófer Reykjalín

Calling Fable from TypeScript

Published on February 5, 2020.

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

  1. Mirror the compiled JavaScript folder hierarchy in your source folder.
  2. Put your declaration files in the same folder as your F# source files.
  3. Name your declaration files <f#_source_name>.js.d.ts.
  4. Import in TypeScript with import { yourFunction } from 'path/to/fsharp/source.js'.