Opinionated Fable2 - Architecture & Performance
Explore Fable2's architecture improvements and performance gains. See how the new compiler generates smaller, optimized JavaScript with better tree shaking.
Nothing can be more beautiful if you wish to come. And it seems Fable2 has made my wish true in most cases. Let's revisit my old blog and check what has improved and changed. So, the goal of this blog is it should be the same as the old blog, but it should be smaller. Because of the new and shiny Fable2.
Fable 2 is currently in beta but as per the blog, it is very much working. So, let's begin

Performance
Line of Code
Fable 2 has great improvements in code generation. Now, the generated code has a better tree shaking story and also less reflection. Let's have a look at old and new code...
F# Types
Let's start with simple F# types. What is there in Fable and what is not with Fable 2?
Let's take a simple example
type InfoModel = {
FirstName : string
LastName : string
DOB : string
Gender : string
IsValid : bool
}
PS: I'm using Fable REPL and Fable 2 REPL for testing out the code. It's a great way you can see what our F# code is doing. It will generate hell pretty JavaScript.
Let's see the generated JavaScript for Fable
import { setType } from "fable-core/Symbol";
import _Symbol from "fable-core/Symbol";
import { compareRecords, equalsRecords } from "fable-core/Util";
export class InfoModel {
constructor(firstName, lastName, dOB, gender, isValid) {
this.FirstName = firstName;
this.LastName = lastName;
this.DOB = dOB;
this.Gender = gender;
this.IsValid = isValid;
}
[_Symbol.reflection]() {
return {
type: "Test.InfoModel",
interfaces: ["FSharpRecord", "System.IEquatable", "System.IComparable"],
properties: {
FirstName: "string",
LastName: "string",
DOB: "string",
Gender: "string",
IsValid: "boolean"
}
};
}
Equals(other) {
return equalsRecords(this, other);
}
CompareTo(other) {
return compareRecords(this, other) | 0;
}
}
setType("Test.InfoModel", InfoModel);
And here is Fable 2
import { declare, Record } from "fable-core/Types";
export const InfoModel = declare(function InfoModel(arg1, arg2, arg3, arg4, arg5) {
this.FirstName = arg1;
this.LastName = arg2;
this.DOB = arg3;
this.Gender = arg4;
this.IsValid = arg5;
}, Record);
Now that is crazy improvement.
Let's add some more code in there.
type InfoModel = {
FirstName : string
LastName : string
DOB : string
Gender : string
IsValid : bool
} with
static member Empty = {
FirstName = "Don"
LastName = "Syme"
DOB = "unknown"
Gender = "male"
IsValid = true
}
A typical F# way of writing a Record Type or Model.
and here is the JavaScript code for Fable
import { setType } from "fable-core/Symbol";
import _Symbol from "fable-core/Symbol";
import { compareRecords, equalsRecords } from "fable-core/Util";
export class InfoModel {
constructor(firstName, lastName, dOB, gender, isValid) {
this.FirstName = firstName;
this.LastName = lastName;
this.DOB = dOB;
this.Gender = gender;
this.IsValid = isValid;
}
[_Symbol.reflection]() {
return {
type: "Test.InfoModel",
interfaces: ["FSharpRecord", "System.IEquatable", "System.IComparable"],
properties: {
FirstName: "string",
LastName: "string",
DOB: "string",
Gender: "string",
IsValid: "boolean"
}
};
}
Equals(other) {
return equalsRecords(this, other);
}
CompareTo(other) {
return compareRecords(this, other) | 0;
}
static get Empty() {
return new InfoModel("Don", "Syme", "unknown", "male", true);
}
}
setType("Test.InfoModel", InfoModel);
And here is the code for Fable 2
import { declare, Record } from "fable-core/Types";
export const InfoModel = declare(function InfoModel(arg1, arg2, arg3, arg4, arg5) {
this.FirstName = arg1;
this.LastName = arg2;
this.DOB = arg3;
this.Gender = arg4;
this.IsValid = arg5;
}, Record);
export function InfoModel$$$get_Empty() {
return new InfoModel("Don", "Syme", "unknown", "male", true);
}

In Fable, we need to use something called [<Pojo>] attribute in code. It is removed in Fable 2. No more dancing around module and type.
In Fable, [<Pojo>] is used like this and generates the below JavaScript. That is default in Fable 2. A request I had in the old blog and very much granted in the latest Fable 2
open FSharp.Core
open Fable.Core
[<Pojo>]
type InfoModel = {
FirstName : string
LastName : string
DOB : string
Gender : string
IsValid : bool
}
module InfoModel =
let Empty = {
FirstName = "Don"
LastName = "Syme"
DOB = "unknown"
Gender = "male"
IsValid = true
}
In JavaScript, it looks like
export const InfoModelModule = function (__exports) {
const Empty = __exports.Empty = {
FirstName: "Don",
LastName: "Syme",
DOB: "unknown",
Gender: "male",
IsValid: true
};
return __exports;
}({});
Now let's see how it looks in Fable 2
type InfoModel = {
FirstName : string
LastName : string
DOB : string
Gender : string
IsValid : bool
} with static member Empty = {
FirstName = "Don"
LastName = "Syme"
DOB = "unknown"
Gender = "male"
IsValid = true
}
And here is the generated code
import { declare, Record } from "fable-core/Types";
export const InfoModel = declare(function InfoModel(arg1, arg2, arg3, arg4, arg5) {
this.FirstName = arg1;
this.LastName = arg2;
this.DOB = arg3;
this.Gender = arg4;
this.IsValid = arg5;
}, Record);
export function InfoModel$$$get_Empty() {
return new InfoModel("Don", "Syme", "unknown", "male", true);
}
No pojo and no modules. Pure types and still get the same result as above. Can't ask for more.
Lenses and Spectacles
The above InfoModel is perfect to represent a view. But what about validation? It is not holding any property to hold domain details. So, let's write that and see the generated code.
open FSharp.Core
open Fable.Core
[<Pojo>]
type Validate = {
IsValid : bool
ErrMsg : string
}
module Validate =
let Success = {
IsValid = true
ErrMsg = ""
}
let Failure (msg : string) = {
IsValid = false
ErrMsg = msg
}
let InitialValidate = {
IsValid = true
ErrMsg = ""
}
[<Pojo>]
type InfoModel = {
FirstName : string
LastName : string
DOB : string
Gender : string
IsValid : bool
}
module InfoModel =
let Empty = {
FirstName = "Don"
LastName = "Syme"
DOB = "unknown"
Gender = "male"
IsValid = true
}
[<Pojo>]
type InfoErrorModel = {
FirstName : Validate
LastName : Validate
DOB : Validate
Gender : Validate
}
module InfoErrorModel =
let Empty = {
FirstName = Validate.InitialValidate
LastName = Validate.InitialValidate
DOB = Validate.InitialValidate
Gender = Validate.InitialValidate
}
[<Pojo>]
type Model = {
InfoModel : InfoModel
InfoErrorModel : InfoErrorModel
}
module Model =
let Empty = {
InfoModel = InfoModel.Empty
InfoErrorModel = InfoErrorModel.Empty
}
As we can see, it is a very typical kind of domain model code. Typical F# - DDD 101 stuff. I will explain domain models in a separate section, but let's first see the generated JavaScript code.
export const ValidateModule = function (__exports) {
const Success = __exports.Success = {
IsValid: true,
ErrMsg: ""
};
const Failure = __exports.Failure = function (msg) {
return {
IsValid: false,
ErrMsg: msg
};
};
const InitialValidate = __exports.InitialValidate = {
IsValid: true,
ErrMsg: ""
};
return __exports;
}({});
export const InfoModelModule = function (__exports) {
const Empty = __exports.Empty = {
FirstName: "Don",
LastName: "Syme",
DOB: "unknown",
Gender: "male",
IsValid: true
};
return __exports;
}({});
export const InfoErrorModelModule = function (__exports) {
const Empty_1 = __exports.Empty = {
FirstName: ValidateModule.InitialValidate,
LastName: ValidateModule.InitialValidate,
DOB: ValidateModule.InitialValidate,
Gender: ValidateModule.InitialValidate
};
return __exports;
}({});
export const ModelModule = function (__exports) {
const Empty_2 = __exports.Empty = {
InfoModel: InfoModelModule.Empty,
InfoErrorModel: InfoErrorModelModule.Empty
};
return __exports;
}({});
Let's see how Fable 2 will improve the F# and JavaScript code.
type Validate = {
IsValid : bool
ErrMsg : string
}
with
static member Success = {
IsValid = true
ErrMsg = ""
}
static member Failure (msg : string) = {
IsValid = false
ErrMsg = msg
}
static member InitialValidate = {
IsValid = true
ErrMsg = ""
}
type InfoModel = {
FirstName : string
LastName : string
DOB : string
Gender : string
IsValid : bool
}
with static member Empty = {
FirstName = "Don"
LastName = "Syme"
DOB = "unknown"
Gender = "male"
IsValid = true
}
type InfoErrorModel = {
FirstName : Validate
LastName : Validate
DOB : Validate
Gender : Validate
}
with static member Empty = {
FirstName = Validate.InitialValidate
LastName = Validate.InitialValidate
DOB = Validate.InitialValidate
Gender = Validate.InitialValidate
}
type Model = {
InfoModel : InfoModel
InfoErrorModel : InfoErrorModel
}
with static member Empty = {
InfoModel = InfoModel.Empty
InfoErrorModel = InfoErrorModel.Empty
}
and the generated JavaScript is
import { declare, Record } from "fable-core/Types";
export const Validate = declare(function Validate(arg1, arg2) {
this.IsValid = arg1;
this.ErrMsg = arg2;
}, Record);
export function Validate$$$get_Success() {
return new Validate(true, "");
}
export function Validate$$$Failure$$Z721C83C5(msg) {
return new Validate(false, msg);
}
export function Validate$$$get_InitialValidate() {
return new Validate(true, "");
}
export const InfoModel = declare(function InfoModel(arg1, arg2, arg3, arg4, arg5) {
this.FirstName = arg1;
this.LastName = arg2;
this.DOB = arg3;
this.Gender = arg4;
this.IsValid = arg5;
}, Record);
export function InfoModel$$$get_Empty() {
return new InfoModel("Don", "Syme", "unknown", "male", true);
}
export const InfoErrorModel = declare(function InfoErrorModel(arg1, arg2, arg3, arg4) {
this.FirstName = arg1;
this.LastName = arg2;
this.DOB = arg3;
this.Gender = arg4;
}, Record);
export function InfoErrorModel$$$get_Empty() {
return new InfoErrorModel(Validate$$$get_InitialValidate(), Validate$$$get_InitialValidate(), Validate$$$get_InitialValidate(), Validate$$$get_InitialValidate());
}
export const Model = declare(function Model(arg1, arg2) {
this.InfoModel = arg1;
this.InfoErrorModel = arg2;
}, Record);
export function Model$$$get_Empty() {
return new Model(InfoModel$$$get_Empty(), InfoErrorModel$$$get_Empty());
}
The generated JavaScript is a little less readable but the code is more concise. As long as everything is in English, I am okay with that.
I still hold the opinion of avoiding
aetherhere. If you need that, then there is a need to revisit what you are doing. Specifically for the Elmish part. Else, lenses are very much okay in most of theserverside code. But still use them with caution.
Domain Domain Domain
Just like everyone in the Functional Domain, I am a big fan of Domain-Driven Design and Scott W. It makes things easier to represent. What is the use of Fable if I can't use it in the front end? And Fable did generate way too much JavaScript code. Let's see how Fable 2 does in this case.
Simple string testing
type Name private(s : string) =
member __.Name = s
static member Create(s: string) =
if (s <> "" || s <> null) then Ok s
else Error "Invalid String"
converts to
import { setType } from "fable-core/Symbol";
import _Symbol from "fable-core/Symbol";
import Result from "fable-core/Result";
export class Name {
[_Symbol.reflection]() {
return {
type: "Test.Name",
properties: {
Name: "string"
}
};
}
constructor(s) {
this.s = s;
}
get Name() {
return this.s;
}
static Create(s) {
if (s !== "" ? true : s != null) {
return new Result(0, s);
} else {
return new Result(1, "Invalid String");
}
}
}
setType("Test.Name", Name);
In case of Fable 2
import { Result } from "fable-core/Option";
import { declare } from "fable-core/Types";
export const Name = declare(function Name(s$$1) {
const $this$$1 = this;
$this$$1.s = s$$1;
});
function Name$$$$002Ector$$Z721C83C5(s$$1) {
return this != null ? Name.call(this, s$$1) : new Name(s$$1);
}
export function Name$$get_Name(__) {
return __.s;
}
export function Name$$$Create$$Z721C83C5(s) {
if (s !== "" ? true : s !== null) {
return new Result(0, "Ok", s);
} else {
return new Result(1, "Error", "Invalid String");
}
}

Let's try it another way
type Name = private | Name of string
with
member this.String = let (Name s) = this in s
static member Create(s : string)=
if (s <> "" || s <> null ) then Ok s
else Error "Invalid string"
converts to
import { setType } from "fable-core/Symbol";
import _Symbol from "fable-core/Symbol";
import { compareUnions, equals } from "fable-core/Util";
import Result from "fable-core/Result";
class Name {
constructor(tag, data) {
this.tag = tag | 0;
this.data = data;
}
[_Symbol.reflection]() {
return {
type: "Test.Name",
interfaces: ["FSharpUnion", "System.IEquatable", "System.IComparable"],
cases: [["Name", "string"]]
};
}
Equals(other) {
return this === other || this.tag === other.tag && equals(this.data, other.data);
}
CompareTo(other) {
return compareUnions(this, other) | 0;
}
get String() {
return this.data;
}
static Create(s) {
if (s !== "" ? true : s != null) {
return new Result(0, s);
} else {
return new Result(1, "Invalid string");
}
}
}
setType("Test.Name", Name);
And in case of Fable 2
import { Result } from "fable-core/Option";
import { declare, Union } from "fable-core/Types";
export const Name = declare(function Name(tag, name, ...fields) {
Union.call(this, tag, name, ...fields);
}, Union);
export function Name$$get_String(this$) {
const s$$1 = this$.fields[0];
return s$$1;
}
export function Name$$$Create$$Z721C83C5(s) {
if (s !== "" ? true : s !== null) {
return new Result(0, "Ok", s);
} else {
return new Result(1, "Error", "Invalid string");
}
}

There is a detailed explanation in my old blog about how DDD is different in the case of server and client. That holds true even now, so you can always check that out.
London Rail
How is ROP going to stand in case of Fable 2? Does that improve also?
Here is code directly copy-pasted from the site with a simple example
open FSharp.Core
open Fable.Core
open System
// convert a single value into a two-track result
let succeed x =
Ok x
// convert a single value into a two-track result
let fail (x) =
Error x
// apply either a success function or failure function
let either successFunc failureFunc twoTrackInput =
match twoTrackInput with
| Ok s -> successFunc s
| Error f -> failureFunc f
// convert a switch function into a two-track function
let bind f =
either f fail
// pipe a two-track value into a switch function
let (>>=) x f =
bind f x
// compose two switches into another switch
let (>=>) s1 s2 =
s1 >> bind s2
// convert a one-track function into a switch
let switch f =
f >> succeed
// convert a one-track function into a two-track function
let map f =
either (f >> succeed) fail
// convert a dead-end function into a one-track function
let tee f x =
f x; x
// convert a one-track function into a switch with exception handling
let tryCatch f exnHandler x =
try
f x |> succeed
with
| ex -> exnHandler ex |> fail
// convert two one-track functions into a two-track function
let doubleMap successFunc failureFunc =
either (successFunc >> succeed) (failureFunc >> fail)
// add two switches in parallel
let plus addSuccess addFailure switch1 switch2 x =
match (switch1 x),(switch2 x) with
| Ok s1, Ok s2 -> Ok (addSuccess s1 s2)
| Error f1, Ok _ -> Error f1
| Ok _ ,Error f2 -> Error f2
| Error f1,Error f2 -> Error (addFailure f1 f2)
let collect errorFn xs =
xs |> Seq.fold (fun res next ->
match res, next with
| Ok r, Ok i -> Ok (i::r)
| Ok _, Error m | Error m, Ok _ -> Error m
| Error m, Error n -> Error (errorFn m n)
) (Ok []) |> map List.rev
let validation1 (s:string) = if (String.IsNullOrEmpty(s)) then Error "String should not be empty" else Ok s
let validation2 (s:string) = if (s.Length > 50) then Error "Can't be more than 50" else Ok s
let combinedValidation = validation1 >> bind validation2
And here is the JavaScript code.
import Result from "fable-core/Result";
import CurriedLambda from "fable-core/CurriedLambda";
import { reverse } from "fable-core/List";
import List from "fable-core/List";
import { fold } from "fable-core/Seq";
import { isNullOrEmpty } from "fable-core/String";
export function succeed(x) {
return new Result(0, x);
}
export function fail(x) {
return new Result(1, x);
}
export function either(successFunc, failureFunc, twoTrackInput) {
if (twoTrackInput.tag === 1) {
return failureFunc(twoTrackInput.data);
} else {
return successFunc(twoTrackInput.data);
}
}
export function bind(f) {
var failureFunc;
return CurriedLambda((failureFunc = function (x) {
return fail(x);
}, function (twoTrackInput) {
return either(f, failureFunc, twoTrackInput);
}));
}
export function op_GreaterGreaterEquals(x, f) {
return bind(f)(x);
}
export function op_GreaterEqualsGreater(s1, s2) {
return CurriedLambda($var1 => bind(s2)(s1($var1)));
}
function _switch(f) {
return CurriedLambda($var2 => function (x) {
return succeed(x);
}(f($var2)));
}
export { _switch as switch };
export function map(f) {
var successFunc;
var failureFunc;
return CurriedLambda((successFunc = $var3 => function (x) {
return succeed(x);
}(f($var3)), failureFunc = function (x_1) {
return fail(x_1);
}, function (twoTrackInput) {
return either(successFunc, failureFunc, twoTrackInput);
}));
}
export function tee(f, x) {
f(x);
return x;
}
export function tryCatch(f, exnHandler, x) {
try {
return succeed(f(x));
} catch (ex) {
return fail(exnHandler(ex));
}
}
export function doubleMap(successFunc, failureFunc) {
var successFunc_1;
var failureFunc_1;
return CurriedLambda((successFunc_1 = $var4 => function (x) {
return succeed(x);
}(successFunc($var4)), failureFunc_1 = $var5 => function (x_1) {
return fail(x_1);
}(failureFunc($var5)), function (twoTrackInput) {
return either(successFunc_1, failureFunc_1, twoTrackInput);
}));
}
export function plus(addSuccess, addFailure, switch1, switch2, x) {
const matchValue = [switch1(x), switch2(x)];
if (matchValue[0].tag === 1) {
if (matchValue[1].tag === 1) {
return new Result(1, addFailure(matchValue[0].data, matchValue[1].data));
} else {
return new Result(1, matchValue[0].data);
}
} else if (matchValue[1].tag === 1) {
return new Result(1, matchValue[1].data);
} else {
return new Result(0, addSuccess(matchValue[0].data, matchValue[1].data));
}
}
export function collect(errorFn, xs) {
return map(function (list) {
return reverse(list);
})(fold(function (res, next) {
const matchValue = [res, next];
const $var6 = matchValue[0].tag === 1 ? matchValue[1].tag === 1 ? [2, matchValue[0].data, matchValue[1].data] : [1, matchValue[0].data] : matchValue[1].tag === 1 ? [1, matchValue[1].data] : [0, matchValue[1].data, matchValue[0].data];
switch ($var6[0]) {
case 0:
return new Result(0, new List($var6[1], $var6[2]));
case 1:
return new Result(1, $var6[1]);
case 2:
return new Result(1, errorFn($var6[1], $var6[2]));
}
}, new Result(0, new List()), xs));
}
export function validation1(s) {
if (isNullOrEmpty(s)) {
return new Result(1, "String should not be empty");
} else {
return new Result(0, s);
}
}
export function validation2(s) {
if (s.length > 50) {
return new Result(1, "Can't be more than 50");
} else {
return new Result(0, s);
}
}
export const combinedValidation = CurriedLambda($var7 => bind(function (s_1) {
return validation2(s_1);
})(function (s) {
return validation1(s);
}($var7)));
and here is the case for Fable 2
import { reverse } from "fable-core/List";
import { Result } from "fable-core/Option";
import { fold } from "fable-core/Seq";
import { isNullOrEmpty } from "fable-core/String";
import { L } from "fable-core/Types";
export function succeed(x$$13) {
return new Result(0, "Ok", x$$13);
}
export function fail(x$$12) {
return new Result(1, "Error", x$$12);
}
export function either(successFunc$$3, failureFunc$$4, twoTrackInput$$3) {
if (twoTrackInput$$3.tag === 1) {
const f$$6 = twoTrackInput$$3.fields[0];
return failureFunc$$4(f$$6);
} else {
const s$$4 = twoTrackInput$$3.fields[0];
return successFunc$$3(s$$4);
}
}
export function bind(f$$5) {
return function (twoTrackInput$$2) {
return either(f$$5, fail, twoTrackInput$$2);
};
}
export function op_GreaterGreaterEquals(x$$10, f$$4) {
return bind(f$$4)(x$$10);
}
export function op_GreaterEqualsGreater(s1$$1, s2$$1) {
return function ($arg$$6) {
return bind(s2$$1)(s1$$1($arg$$6));
};
}
export function switch$(f$$3) {
return function ($arg$$5) {
return succeed(f$$3($arg$$5));
};
}
export function map(f$$2) {
return function (twoTrackInput$$1) {
return either(function successFunc$$2($arg$$4) {
return succeed(f$$2($arg$$4));
}, fail, twoTrackInput$$1);
};
}
export function tee(f$$1, x$$6) {
f$$1(x$$6);
return x$$6;
}
export function tryCatch(f, exnHandler, x$$3) {
try {
return succeed(f(x$$3));
} catch (ex) {
return fail(exnHandler(ex));
}
}
export function doubleMap(successFunc, failureFunc) {
return function (twoTrackInput) {
return either(function successFunc$$1($arg$$3) {
return succeed(successFunc($arg$$3));
}, function failureFunc$$1($arg$$2) {
return fail(failureFunc($arg$$2));
}, twoTrackInput);
};
}
export function plus(addSuccess, addFailure, switch1, switch2, x) {
const matchValue$$1 = [switch1(x), switch2(x)];
if (matchValue$$1[0].tag === 1) {
if (matchValue$$1[1].tag === 1) {
return new Result(1, "Error", addFailure(matchValue$$1[0].fields[0], matchValue$$1[1].fields[0]));
} else {
return new Result(1, "Error", matchValue$$1[0].fields[0]);
}
} else if (matchValue$$1[1].tag === 1) {
return new Result(1, "Error", matchValue$$1[1].fields[0]);
} else {
return new Result(0, "Ok", addSuccess(matchValue$$1[0].fields[0], matchValue$$1[1].fields[0]));
}
}
export function collect(errorFn, xs) {
return map(reverse)(fold(function folder(res, next) {
const matchValue = [res, next];
var $target$$9, i, r, m, m$$1, n;
if (matchValue[0].tag === 1) {
if (matchValue[1].tag === 1) {
$target$$9 = 2;
m$$1 = matchValue[0].fields[0];
n = matchValue[1].fields[0];
} else {
$target$$9 = 1;
m = matchValue[0].fields[0];
}
} else if (matchValue[1].tag === 1) {
$target$$9 = 1;
m = matchValue[1].fields[0];
} else {
$target$$9 = 0;
i = matchValue[1].fields[0];
r = matchValue[0].fields[0];
}
switch ($target$$9) {
case 0:
{
return new Result(0, "Ok", L(i, r));
break;
}
case 1:
{
return new Result(1, "Error", m);
break;
}
case 2:
{
return new Result(1, "Error", errorFn(m$$1, n));
break;
}
}
}, new Result(0, "Ok", L()), xs));
}
export function validation1(s$$3) {
if (isNullOrEmpty(s$$3)) {
return new Result(1, "Error", "String should not be empty");
} else {
return new Result(0, "Ok", s$$3);
}
}
export function validation2(s$$2) {
if (s$$2.length > 50) {
return new Result(1, "Error", "Can't be more than 50");
} else {
return new Result(0, "Ok", s$$2);
}
}
export function combinedValidation($arg$$1) {
return bind(validation2)(validation1($arg$$1));
}
Similar code. I guess that is the reason that having simple pure functions is always a good thing for the application. So, not much of a visible improvement in this part.
Slice and Dice
Fable 2 is not much good without Elmish. Let's give it a shot with an Elmish example.
This is Fable code.
[<Pojo>]
type Validate = {
IsValid : bool
ErrMsg : string
}
module Validate =
let Success = {
IsValid = true
ErrMsg = ""
}
let Failure (msg : string) = {
IsValid = false
ErrMsg = msg
}
let InitialValidate = {
IsValid = true
ErrMsg = ""
}
[<Pojo>]
type InfoModel = {
FirstName : string
FirstNameErr : Validate
LastName : string
LastNameErr : Validate
DOB : string
DOBErr : Validate
Gender : string
GenderErr : Validate
IsValid : bool
}
[<Pojo>]
type ContactModel = {
Mobile : string
MobileErr : Validate
Home : string
HomeErr : Validate
Office : string
OfficeErr : Validate
Email : string
EmailErr : Validate
Email2 : string
Email2Err : Validate
IsValid : bool
}
[<Pojo>]
type Model = {
Info : InfoModel
Contact : ContactModel
}
In Fable 2, it would be written like this
type Validate = {
IsValid : bool
ErrMsg : string
} with
static member Success = {
IsValid = true
ErrMsg = ""
}
static member Failure (msg : string) = {
IsValid = false
ErrMsg = msg
}
static member InitialValidate = {
IsValid = true
ErrMsg = ""
}
type InfoModel = {
FirstName : string
FirstNameErr : Validate
LastName : string
LastNameErr : Validate
DOB : string
DOBErr : Validate
Gender : string
GenderErr : Validate
IsValid : bool
}
type ContactModel = {
Mobile : string
MobileErr : Validate
Home : string
HomeErr : Validate
Office : string
OfficeErr : Validate
Email : string
EmailErr : Validate
Email2 : string
Email2Err : Validate
IsValid : bool
}
type Model = {
Info : InfoModel
Contact : ContactModel
}
Here, consider we have two pages Info and Contact. And then we have an update method to update them. Pretty straightforward.
Until we have around 20 pages or 50 components on the same page
For the first time, I felt what slowness is while typing in a simple HTML input box. Things just dragged like we are typing in a WPF application on a Windows 98 machine.
Let's understand what is happening here. The model is holding information required for Views. Now, even though the Info view is active, it is having information about Contact also. This makes the model quite big. Specifically if there are three or four levels of deep models. Deep models make things easy to understand but quite difficult to update. The deeper and bigger the model is, the slower the update of property is. Now, in ELM / Elmish architecture, we rely on the model extensively for what we can see on the view. Even simple typing into an input requires us to update the model. And then there is validation and other stuff going on.
It also presents another weird problem. When we start the application, there is a need to initialize the models. So, for a big application, one needs to set all models at the start. To do that, we need to fire up too many requests to the server. For a sec, we consider that would be OK in the era of cloud, but it can't resolve the issue of dependent types. If some property is dependent on another, then we can't have them at the start, and we can't initialize the model.
No model, No view.
What is the alternative to this fat model?
Slim Union Type - Here is the above code written differently using Fable
open FSharp.Core
open Fable.Core
open System
[<Pojo>]
type Validate = {
IsValid : bool
ErrMsg : string
}
module Validate =
let Success = {
IsValid = true
ErrMsg = ""
}
let Failure (msg : string) = {
IsValid = false
ErrMsg = msg
}
let InitialValidate = {
IsValid = true
ErrMsg = ""
}
[<Pojo>]
type InfoModel = {
FirstName : string
FirstNameErr : Validate
LastName : string
LastNameErr : Validate
DOB : string
DOBErr : Validate
Gender : string
GenderErr : Validate
IsValid : bool
}
[<Pojo>]
type ContactModel = {
Mobile : string
MobileErr : Validate
Home : string
HomeErr : Validate
Office : string
OfficeErr : Validate
Email : string
EmailErr : Validate
Email2 : string
Email2Err : Validate
IsValid : bool
}
type Page = Info = 0 | Contact = 1
type PageModel = Info of InfoModel | Contact of ContactModel
[<Pojo>]
type Model = {
Page : Page
PageModel : PageModel
}
Here is the generated code for the second option
import { setType } from "fable-core/Symbol";
import _Symbol from "fable-core/Symbol";
import { compareUnions, equals, Any } from "fable-core/Util";
export const ValidateModule = function (__exports) {
const Success = __exports.Success = {
IsValid: true,
ErrMsg: ""
};
const Failure = __exports.Failure = function (msg) {
return {
IsValid: false,
ErrMsg: msg
};
};
const InitialValidate = __exports.InitialValidate = {
IsValid: true,
ErrMsg: ""
};
return __exports;
}({});
export class PageModel {
constructor(tag, data) {
this.tag = tag | 0;
this.data = data;
}
[_Symbol.reflection]() {
return {
type: "Test.PageModel",
interfaces: ["FSharpUnion", "System.IEquatable", "System.IComparable"],
cases: [["Info", Any], ["Contact", Any]]
};
}
Equals(other) {
return this === other || this.tag === other.tag && equals(this.data, other.data);
}
CompareTo(other) {
return compareUnions(this, other) | 0;
}
}
setType("Test.PageModel", PageModel);
Let's check the Fable 2 option
open FSharp.Core
open Fable.Core
open System
type Validate = {
IsValid : bool
ErrMsg : string
}with
static member Success = {
IsValid = true
ErrMsg = ""
}
static member Failure (msg : string) = {
IsValid = false
ErrMsg = msg
}
static member InitialValidate = {
IsValid = true
ErrMsg = ""
}
type InfoModel = {
FirstName : string
FirstNameErr : Validate
LastName : string
LastNameErr : Validate
DOB : string
DOBErr : Validate
Gender : string
GenderErr : Validate
IsValid : bool
}
type ContactModel = {
Mobile : string
MobileErr : Validate
Home : string
HomeErr : Validate
Office : string
OfficeErr : Validate
Email : string
EmailErr : Validate
Email2 : string
Email2Err : Validate
IsValid : bool
}
type Page = Info | Contact
type PageModel = Info of InfoModel | Contact of ContactModel
type Model = {
Page : Page
PageModel : PageModel
}
and here is the generated JavaScript
import { Union, declare, Record } from "fable-core/Types";
export const Validate = declare(function Validate(arg1, arg2) {
this.IsValid = arg1;
this.ErrMsg = arg2;
}, Record);
export function Validate$$$get_Success() {
return new Validate(true, "");
}
export function Validate$$$Failure$$Z721C83C5(msg) {
return new Validate(false, msg);
}
export function Validate$$$get_InitialValidate() {
return new Validate(true, "");
}
export const InfoModel = declare(function InfoModel(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9) {
this.FirstName = arg1;
this.FirstNameErr = arg2;
this.LastName = arg3;
this.LastNameErr = arg4;
this.DOB = arg5;
this.DOBErr = arg6;
this.Gender = arg7;
this.GenderErr = arg8;
this.IsValid = arg9;
}, Record);
export const ContactModel = declare(function ContactModel(arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11) {
this.Mobile = arg1;
this.MobileErr = arg2;
this.Home = arg3;
this.HomeErr = arg4;
this.Office = arg5;
this.OfficeErr = arg6;
this.Email = arg7;
this.EmailErr = arg8;
this.Email2 = arg9;
this.Email2Err = arg10;
this.IsValid = arg11;
}, Record);
export const Page = declare(function Page(tag, name, ...fields) {
Union.call(this, tag, name, ...fields);
}, Union);
export const PageModel = declare(function PageModel(tag, name, ...fields) {
Union.call(this, tag, name, ...fields);
}, Union);
export const Model = declare(function Model(arg1, arg2) {
this.Page = arg1;
this.PageModel = arg2;
}, Record);
Here, you can see, I don't have to define Union as Enum just to get optimized JavaScript.
Want to save a few more lines?
Try this
type [<StringEnum>]Page = Info | Contact
The Page part will be removed from the generated code. As it will directly compare strings when and if required.
Old Habits Die Hard
JS interop is working as it was working earlier. And so does my personal favorite, Fulma.
Shameless Plug

After working with many big and small consulting companies on various projects ranging from dotnet web to Node.js, single page applications to cross-platform mobile applications, I decided to go solo. There were hiccups at the start, but I did survive for almost a year. Not only did I survive, but I also started my own consulting company Fuzzy Cloud.
So, I am looking for new assignments. If anyone is interested in work or training with me, please contact me. I know we are so small compared to other friends from Europe and the USA, but we still share the same love for F# and functional programming in general. And I am always ready for challenging assignments.
I'd love to have feedback about this article and will update it if there are any additions or changes required.
Frequently Asked Questions
Fable 2 significantly reduces generated JavaScript code size through better tree shaking and less reflection overhead. For example, a simple F# record type that generated over 40 lines of JavaScript in Fable now compiles to just 5-6 lines in Fable 2, making it a dramatic improvement in code efficiency.
Fable 2 uses a new `declare` function from `fable-core/Types` to generate cleaner record type definitions without the reflection metadata and utility function imports required in the original Fable. This results in more readable and compact JavaScript output.
While Fable 2 is currently in beta, it is very much working and usable according to the official blog. The improvements in code generation and performance make it a solid choice for F# to JavaScript compilation projects.
Yes, you can use the Fable 2 REPL (available at fable.io/repl2/) to test your F# code and see the generated JavaScript output in real-time. This is a great way to understand how your F# code compiles and compare it with previous versions.