Interacting with JavaScript

Fable features for easy interoperability

There are several ways to interact with the JavaScript world:

  • Dynamically
  • Using Emit attribute
  • Through a foreign interface
  • Special attributes

Dynamic programming

Fable.Core implements the F# dynamic operators so you can easily access an object property by name (without static check) as follows:

1: 
2: 
3: 
4: 
5: 
open Fable.Core

printfn "Value: %O" jsObject?myProperty

jsObject?myProperty <- 5 // Assignment is also possible

However, the F# compiler won't let you apply the property directly to other expressions. For that, use the $ operator and pass the arguments as a tuple.

1: 
2: 
3: 
open Fable.Core

let result = jsObject?myMethod $ (1, 2)

If you want to call the function with the new keyword, use Fable.Core.createNew instead.

1: 
2: 
3: 
open Fable.Core

let instance = createNew jsObject?method (1, 2)

And when you need to create JS object literals, use createObj:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
9: 
open Fable.Core

let data =
    createObj [
        "todos" ==> Storage.fetch()
        "newTodo" ==> ""
        "editedTodo" ==> None
        "visibility" ==> "all"
    ]

The todomvc sample is a good example on how to program dynamically with Fable.

Emit attribute

You can use the Emit attribute to decorate a function. Every call to the function will then be replaced inline by the content of the attribute with the placeholders $0, $1, $2... replaced by the arguments. For example, the following code will generate JavaScript as seen below.

1: 
2: 
3: 
4: 
5: 
6: 
open Fable.Core

[<Emit("$0 + $1")>]
let add (x: int) (y: string): float = failwith "JS only"

let result = add 1 "2"
1: 
var result = 1 + "2"

When you don't know the exact number of arguments you can use the following syntax:

1: 
2: 
3: 
type Test() =
    [<Emit("$0($1...)")>]
    member __.Invoke([<ParamArray>] args: int[]): obj = failwith "JS only"

It's also possible to pass syntax conditioned to optional parameters.

1: 
2: 
3: 
type Test() =
    [<Emit("$0[$1]{{=$2}}")>]
    member __.Item with get(): float = failwith "JS only" and set(v: float): unit = failwith "JS only"

The content of Emit will actually be parsed by Babel so it will still be validated somehow. However, it's not advised to abuse this method, as the code in the template will remain obscure to Fable and may prevent some optimizations.

Foreign interfaces

Defining a foreign interface is trivial: just create a F# interface and the compiler will call its properties or methods by name. The tricky part is to tell the compiler where the objects should be retrieved from. Normally, they will be exposed as values of an imported module, so you just need to indicate the compiler where this module is coming from using the Import attribute (see below). For example, if you want to use string_decoder from node, just write:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
[<Import("*","string_decoder")>]
module string_decoder =
    type NodeStringDecoder =
        abstract write: buffer: Buffer -> strings
        abstract detectIncompleteChar: buffer: Buffer -> float

    let StringDecoder: NodeStringDecoder = failwith "JS only"

If the module or value is globally accessible in JavaScript, you can use the Global attribute without parameters instead. If a method accepts a lambda make sure to use System.Func in the signature to force the compiler uncurry any lambda passed as parameter.

A good starting point for foreign interfaces are Typescript definition files and there's a script to make the bulk work of translating the file into F#. You can install it from npm. See the README for more information.

1: 
npm install -g ts2fable

You can find common definitions already parsed here. Some of them are available in npm, just search for fable-import packages.

Import attribute

The Import attribute can be applied to modules, types and even functions. It will translate to ES2015 import statements, which can be later transformed to commonjs, amd or umd imports by Babel.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
// Namespace imports
[<Import("*", from="my-module")>]          // F#
import * from "my-module"                  // JS

// Member imports
[<Import("myFunction", from="my-module")>] // F#
import { myFunction } from "my-module"     // JS

// Default imports
[<Import("default", from="express")>]      // F#
import express from express                // JS

Special attributes

Besides Emit, Import and Global attributes, there are some attributes available in the Fable.Core namespace to ease the interaction with JS in some particular cases.

Erase attribute

In TypeScript there's a concept of Union Types which differs from union types in F#. The former are just used to statically check a function argument accepting different types. In Fable, they're translated as Erased Union Types whose cases must have one and only one single data field. After compilation, the wrapping will be erased and only the data field will remain. To define an erased union type, just attach the Erase attribute to the type. Example:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
open Fable.Core

[<Erase>]
type MyErasedType =
    | String of string
    | Number of int

myLib.myMethod(String "test")
1: 
myLib.myMethod("test")

Fable.Core already includes predefined erased types which can be used as follows:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
open Fable.Core

type Test() =
    member x.Value = "Test"

let myMethod (arg: U3<string, int, Test>) =
    match arg with
    | U3.Case1 s -> s
    | U3.Case2 i -> string i
    | U3.Case3 t -> t.Value

StringEnum attribute

Similarly, in TypeScript it's possible to define String Literal Types which are similar to enumerations with an underlying string value. Fable allows the same feature by using union types and the StringEnum attribute. These union types must not have any data fields as they will be compiled to a string matching the name of the union case.

By default, the compiled string will have the first letter lowered. If you want to prevent this or use a different text than the union case name, use the CompiledName attribute:

1: 
2: 
3: 
4: 
5: 
6: 
7: 
8: 
open Fable.Core

[<StringEnum>]
type MyStrings =
    | Vertical
    | [<CompiledName("Horizontal")>] Horizontal

myLib.myMethod(Vertical, Horizontal)
1: 
myLib.myMethod("vertical", "Horizontal")

KeyValueList attribute

Many JS libraries accept a plain object to specify different options. With Fable, you can use union types to define these options in a more static-safe and F#-idiomatic manner. The union cases of a type with the KeyValueList attribute act as a key value pair, so they should have a single data field. (If there's no data field the value is assumed to be true.) When Fable encounters a list of such an union type, it will compile it as a plain JS object.

As with StringEnum the first letter of the key (the union case name) will be lowered. Again, you can modify this behaviour with the CompiledName attribute.

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
11: 
12: 
13: 
open Fable.Core

[<KeyValueList>]
type MyOptions =
    | Flag1
    | Name of string
    | [<CompiledName("QTY")>] QTY of int

myLib.myMethod [
    Name "Fable"
    QTY 5
    Flag1
]
1: 
2: 
3: 
4: 
5: 
myLib.myMethod({
    name: "Fable",
    QTY: 5,
    flag1: true
})

If necessary you can cheat the compiler using tuples:

1: 
myLib.myMethod [Name "Fable"; unbox("level", 4)]
1: 
myLib.myMethod({ name: "Fable", level: 4 })

As these lists will be compiled as JS objects, please note you cannot apply the usual list operations to them (e.g. appending). If you want to manipulate the "fake" lists you must implement the methods yourself. For example:

 1: 
 2: 
 3: 
 4: 
 5: 
 6: 
 7: 
 8: 
 9: 
10: 
[<KeyValueList>]
type CSSProp =
    | Border of string
    | Display of string

[<Emit("Object.assign({}, $0, $1)")>]
let ( ++ ) (a:'a list) (b:'a list) : 'a list = failwith "JS Only"

let niceBorder = [ Border "1px solid blue" ]
let style = [ Display "inline-block" ] ++ niceBorder
namespace Microsoft.FSharp.Core
val printfn : format:Printf.TextWriterFormat<'T> -> 'T

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.printfn
val result : obj

Full name: interacting.result
val instance : 'a

Full name: interacting.instance
val data : 'a

Full name: interacting.data
union case Option.None: Option<'T>
val add : x:int -> y:string -> float

Full name: interacting.add
val x : int
Multiple items
val int : value:'T -> int (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.int

--------------------
type int = int32

Full name: Microsoft.FSharp.Core.int

--------------------
type int<'Measure> = int

Full name: Microsoft.FSharp.Core.int<_>
val y : string
Multiple items
val string : value:'T -> string

Full name: Microsoft.FSharp.Core.Operators.string

--------------------
type string = System.String

Full name: Microsoft.FSharp.Core.string
Multiple items
val float : value:'T -> float (requires member op_Explicit)

Full name: Microsoft.FSharp.Core.Operators.float

--------------------
type float = System.Double

Full name: Microsoft.FSharp.Core.float

--------------------
type float<'Measure> = float

Full name: Microsoft.FSharp.Core.float<_>
val failwith : message:string -> 'T

Full name: Microsoft.FSharp.Core.Operators.failwith
val result : float

Full name: interacting.result
Multiple items
type Test =
  new : unit -> Test
  member Invoke : args:int [] -> obj

Full name: interacting.Test

--------------------
new : unit -> Test
member Test.Invoke : args:int [] -> obj

Full name: interacting.Test.Invoke
val args : int []
type obj = System.Object

Full name: Microsoft.FSharp.Core.obj
val __ : Test
val set : elements:seq<'T> -> Set<'T> (requires comparison)

Full name: Microsoft.FSharp.Core.ExtraTopLevelOperators.set
type unit = Unit

Full name: Microsoft.FSharp.Core.unit
type NodeStringDecoder =
  interface
    abstract member detectIncompleteChar : buffer:'a0 -> float
    abstract member write : buffer:'a0 -> 'a1
  end

Full name: interacting.string_decoder.NodeStringDecoder
abstract member NodeStringDecoder.write : buffer:'a0 -> 'a1

Full name: interacting.string_decoder.NodeStringDecoder.write
abstract member NodeStringDecoder.detectIncompleteChar : buffer:'a0 -> float

Full name: interacting.string_decoder.NodeStringDecoder.detectIncompleteChar
val StringDecoder : NodeStringDecoder

Full name: interacting.string_decoder.StringDecoder
type MyErasedType =
  | String of string
  | Number of int

Full name: interacting.MyErasedType
Multiple items
union case MyErasedType.String: string -> MyErasedType

--------------------
module String

from Microsoft.FSharp.Core
union case MyErasedType.Number: int -> MyErasedType
module String

from Microsoft.FSharp.Core
val myMethod : arg:'a -> 'b

Full name: interacting.myMethod
val arg : 'a
type MyStrings =
  | Vertical
  | Horizontal

Full name: interacting.MyStrings
union case MyStrings.Vertical: MyStrings
Multiple items
type CompiledNameAttribute =
  inherit Attribute
  new : compiledName:string -> CompiledNameAttribute
  member CompiledName : string

Full name: Microsoft.FSharp.Core.CompiledNameAttribute

--------------------
new : compiledName:string -> CompiledNameAttribute
union case MyStrings.Horizontal: MyStrings
type MyOptions =
  | Flag1
  | Name of string
  | QTY of int

Full name: interacting.MyOptions
union case MyOptions.Flag1: MyOptions
union case MyOptions.Name: string -> MyOptions
union case MyOptions.QTY: int -> MyOptions
val unbox : value:obj -> 'T

Full name: Microsoft.FSharp.Core.Operators.unbox
type 'T list = List<'T>

Full name: Microsoft.FSharp.Collections.list<_>
Fork me on GitHub