login
RepoCritics — Review. Share. Archive. Every open-source repo.

fable-compiler/Fable

Wiki: fable-compiler/Fable

Source: https://github.com/fable-compiler/Fable

Last synced 2026-07-18 · 1178 words · Edit wiki on GitHub →

fable-compiler/Fable

> The F# to JavaScript (and TypeScript, Python, Rust, Dart, Erlang) compiler — > F# as a first-class citizen of other language ecosystems.

GitHub repo · Official website · License: MIT

Overview

Fable compiles F# source to idiomatic JavaScript, letting .NET developers write browser frontends without leaving the language[^1]. Started in early 2016 by Alfonso García-Caro, the name is a nod to Babel: early Fable emitted a Babel AST ("F# |> BABEL" was the original tagline). Since Fable 3 (2020) it is a self-contained dotnet tool that emits plain ES-module .fs.js files consumed by any bundler with no Babel dependency[^2]. Fable 4 widened the story from "F# to JS" to "F# to X": TypeScript, Python, Rust, and Dart backends (the non-JS ones officially beta), with an Erlang/Beam target as the most recent addition per the repo's own description.

The defining tension is semantic fidelity vs. output cost. Fable does not ship a .NET runtime to the browser (that is Blazor's trade); it reimplements F# core semantics — immutable lists, Map/Set, int64, decimal, structural equality, string formatting — as a per-target fable-library. You get readable, tree-shakeable output, but you inherit an emulation tax wherever F# semantics diverge from the host platform's.

At ~3.1k stars and 329 forks it is a niche-language tool, not a mass-market one — but it is the center of gravity of the F# frontend ecosystem (Elmish, Feliz, the SAFE stack[^3]) and actively maintained, with pushes landing in July 2026 and a moderate 165 open issues.

Getting Started

dotnet new tool-manifest          # once per repo
dotnet tool install fable
dotnet fable src/App.fsproj --outDir dist    # or: dotnet fable watch
// App.fs — needs the Fable.Browser.Dom NuGet package
module App

open Browser.Dom

let mutable count = 0
let button = document.querySelector "#inc" :?> Browser.Types.HTMLButtonElement

button.onclick <- fun _ ->
    count <- count + 1
    document.getElementById("count").textContent <- string count

Fable only produces JS files; a bundler/dev server (Vite is the common pairing) serves and packages them. Most real apps start from an Elmish or SAFE stack template rather than raw DOM calls[^3].

Architecture / How It Works

The frontend is F# Compiler Services (FCS) — but not stock FCS. Fable uses a patched fork (ncave/fsharp) whose binaries are vendored in lib/fcs[^4]. This lets the compiler run in unusual hosts (see fable-standalone below) at the cost of a second F# compiler in the ecosystem: your IDE type-checks with real FCS, Fable compiles with the fork, and the two occasionally disagree on bleeding-edge language features until the fork catches up.

The pipeline: FCS parses and type-checks the .fsproj → the typed AST is transformed into Fable AST (published as the Fable.AST package) → a per-language backend applies target-specific transforms and prints source. Python, Rust, and Dart each get their own printer plus their own port of fable-library (-py, -rust, -dart), because "what is an F# list" has a different answer in each target. The Rust backend is the most ambitious, mapping GC semantics onto reference counting.

Interop lives in Fable.Core: [<Import>] and [<Emit>] attributes, the dynamic ? operator, [<Erase>] unions and [<StringEnum>] for zero-cost typed wrappers over untyped JS. fable-standalone is the compiler compiled to JS by itself, powering the browser-only REPL[^5]. Compiler plugins (e.g. Feliz's JSX-style transform) operate on Fable AST and are binary-coupled to the Fable.AST version — a recurring break point across major releases.

Production Notes

  • The emulation tax is real but bounded. F# lists, Map, Set, int64,

decimal, and sprintf-style formatting are library implementations, not native JS. int64/decimal arithmetic in hot loops is markedly slower than number math; prefer native ints/floats and ResizeArray (a JS array) in performance-critical paths.

  • Structural equality is a function call. = on records/unions compiles

to a deep-equality helper, not === — a surprise when Fable values end up as React deps or keys in identity-keyed JS collections.

  • BCL coverage is partial. Reflection is limited, regex is the host's

engine with different semantics from .NET's, DateTime edge cases differ. Check the compatibility docs before porting server-side F# wholesale[^6].

  • Bindings are the interop bottleneck. Popular libraries have maintained

bindings (Fable.React, Feliz); the long tail means writing your own or running ts2fable over .d.ts files, whose output needs hand repair[^7].

  • Upgrade pains follow major versions. Fable 2→3 discarded the entire

Babel/webpack-loader plugin ecosystem for the dotnet-tool model; Fable 3→4 required plugin authors to recompile against the new Fable.AST. Pin the tool version in the manifest and upgrade deliberately.

  • Treat non-JS targets as beta, because they are. JS/TS is the production

path; Python, Rust, Dart have known gaps, and Erlang/Beam is newer still.

  • Cold compiles pay .NET startup plus type-checking. dotnet fable watch

is the workable inner loop, but large solutions feel slower than esbuild-class JS tooling.

When to Use / When Not

Use when:

  • Your team already writes F# and wants to share domain types and logic

between a .NET backend and a browser frontend (Fable.Remoting, SAFE stack).

  • You want Elmish-style MVU with a mature typed-FP language and .NET tooling.
  • You want readable JS that plugs into a standard bundler, not a runtime.

Avoid when:

  • The team has no F# background and just wants typed JS — TypeScript or

ReScript reach the same goal without a second toolchain.

  • You need full .NET runtime semantics in the browser — Blazor/Bolero ship

the actual runtime instead of emulating it.

  • The app is mostly thin glue over many JS libraries — the binding tax

dominates the benefit.

  • You were hoping to ship the Python/Rust/Dart targets to production today.

Alternatives

  • fsbolero/Bolero — use when you want the real .NET runtime in the browser

(Blazor WebAssembly) instead of transpiled JS.

  • dotnet-websharper/core — older full-stack F#/C# web framework with its own

JS translation; heavier, more framework-shaped.

  • rescript-lang/rescript-compiler — typed functional-to-JS without the .NET

dependency; leaner toolchain, no BCL emulation.

  • elm/compiler — the MVU language Elmish borrowed from; simpler, more

locked-down, no .NET interop.

  • microsoft/TypeScript — stay inside the JS ecosystem when gradual typing is

enough and language-level FP is not the point.

History

| Version | Date | Notes | |---------|------|-------| | 0.x | 2016-01 | Project started; compiled F# to a Babel AST. | | 1.0 | 2017 | First stable line; Babel-based toolchain. | | 2.0 | 2018-10 | Rewrite focused on smaller, more idiomatic JS output. | | 3.0 "Nagareyama" | 2020-12 | Standalone dotnet tool; emits ES modules directly, Babel dependency dropped[^2]. | | 4.0 "Snake Island" | 2023 | Multi-target: TypeScript, Python, Rust, Dart (beta) via --lang; JSX support. |

References

[^1]: Fable homepage. https://fable.io/ [^2]: Fable blog (Fable 3 "Nagareyama" announcements, 2020). https://fable.io/blog/ [^3]: SAFE Stack — full-stack F# (Saturn + Azure + Fable + Elmish). https://safe-stack.github.io/ [^4]: Fable README, FCS fork note. https://github.com/ncave/fsharp/pull/2 [^5]: Fable REPL (fable-standalone in the browser). https://fable.io/repl/ [^6]: Fable docs, .NET/F# compatibility. https://fable.io/docs/ [^7]: ts2fable — generate Fable bindings from TypeScript declarations. https://github.com/fable-compiler/ts2fable

Tags

fsharp, javascript, typescript, python, rust, dart, compiler, transpiler, dotnet, functional-programming, frontend