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
Code language: plaintext (plaintext)
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
Code language: plaintext (plaintext)
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
Code language: plaintext (plaintext)
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
Code language: plaintext (plaintext)
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
Code language: F# (fsharp)
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;
}
Code language: TypeScript (typescript)
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 ): string;
Code language: TypeScript (typescript)
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
Code language: F# (fsharp)
/** helloWorld.js.d.ts */
export class HelloMessage {
name: string;
message: string;
}
export function helloWorld( msg: HelloMessage ): string
Code language: TypeScript (typescript)
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'
.
Leave a Reply