Skip to content

Classes

This page covers F# classes that JavaScript code sees directly: subclasses of JavaScript classes such as custom elements, ES #private fields, native decorators, symbol-keyed members, new.target, and predictable method names. You need it when a platform or a library calls into your class, rather than only your own F# code.

Subclasses of JavaScript classes

By default, Fable compiles the members of an F# class to module functions next to the class, Counter__Increment(counter), and only attaches them to the class with [<AttachMembers>]. That breaks a class that the platform calls into, such as a custom element: the browser looks for connectedCallback and static observedAttributes on the class itself.

In Fickl, a class that inherits a global, imported or [<AttachMembers>] class attaches its members without an attribute. Methods, properties and static members become members of the JavaScript class.

open Fable.Core

[<Global>]
type HTMLElement() =
    member _.setAttribute(name: string, value: string) : unit = jsNative

type CounterElement() =
    inherit HTMLElement()
    let mutable count = 0

    static member observedAttributes = [| "count" |]

    member this.connectedCallback() =
        this.setAttribute("role", "status")

    member _.attributeChangedCallback(name: string, _old: string, value: string) =
        if name = "count" then count <- int value

    member _.Count = count
import { parse } from "@fable-org/fable-library-js/Int32.js";
import { class_type } from "@fable-org/fable-library-js/Reflection.js";

export class CounterElement extends HTMLElement {
    #count;
    constructor() {
        super();
        this.#count = 0;
    }
    static get observedAttributes() {
        return ["count"];
    }
    connectedCallback() {
        const this$ = this;
        this$.setAttribute("role", "status");
    }
    attributeChangedCallback(name, _old, value) {
        const _ = this;
        if (name === "count") {
            _.#count = (parse(value, 66047, false, 32) | 0);
        }
    }
    get Count() {
        const _ = this;
        return _.#count | 0;
    }
}

export function CounterElement_$reflection() {
    return class_type("Example1.CounterElement", undefined, CounterElement, class_type("Example1.HTMLElement", undefined, HTMLElement));
}

export function CounterElement_$ctor() {
    return new CounterElement();
}
import type { MutableArray } from "@fable-org/fable-library-ts/Util.ts";
import { type int32, parse } from "@fable-org/fable-library-ts/Int32.ts";
import { class_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";

export class CounterElement extends HTMLElement {
    #count: int32;
    constructor() {
        super();
        this.#count = 0;
    }
    static get observedAttributes(): MutableArray<string> {
        return ["count"];
    }
    connectedCallback(): void {
        const this$: CounterElement = this;
        this$.setAttribute("role", "status");
    }
    attributeChangedCallback(name: string, _old: string, value: string): void {
        const _: CounterElement = this;
        if (name === "count") {
            _.#count = (parse(value, 66047, false, 32) | 0);
        }
    }
    get Count(): int32 {
        const _: CounterElement = this;
        return _.#count | 0;
    }
}

export function CounterElement_$reflection(): TypeInfo {
    return class_type("Example1.CounterElement", undefined, CounterElement, class_type("Example1.HTMLElement", undefined, HTMLElement));
}

export function CounterElement_$ctor(): CounterElement {
    return new CounterElement();
}

The let field count is also an ES #private field, this.#count, so it cannot set or shadow a property of the base class. See the next section.

To keep the members of such a class as module functions, mark it [<Fickl.Core.AttachMembers(false)>]. [<Fickl.Core.AttachMembers>] and [<Fickl.Core.AttachMembers(true)>] are the same as Fable.Core's attribute. As with Fable's attribute, overloads of attached members do not work.

open Fable.Core

[<Global>]
type EventTarget() =
    class
    end

[<Fickl.Core.AttachMembers(false)>]
type Detached() =
    inherit EventTarget()
    member _.Value = 5
import { class_type } from "@fable-org/fable-library-js/Reflection.js";

export class Detached extends EventTarget {
    constructor() {
        super();
    }
}

export function Detached_$reflection() {
    return class_type("Example2.Detached", undefined, Detached, class_type("Example2.EventTarget", undefined, EventTarget));
}

export function Detached_$ctor() {
    return new Detached();
}

export function Detached__get_Value(_) {
    return 5;
}
import { class_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";

export class Detached extends EventTarget {
    constructor() {
        super();
    }
}

export function Detached_$reflection(): TypeInfo {
    return class_type("Example2.Detached", undefined, Detached, class_type("Example2.EventTarget", undefined, EventTarget));
}

export function Detached_$ctor(): Detached {
    return new Detached();
}

export function Detached__get_Value(_: Detached): int32 {
    return 5;
}

Private fields: [<PrivateFields>] and --privateFields

Fable compiles the let fields of a class to ordinary properties. In a subclass of HTMLElement, a field named title or hidden then sets the element's own property, and in a subclass of Map a field named size fails, as Map.size has only a getter. [<PrivateFields>] compiles the class's let fields to ES #private fields, this.#balance, which cannot collide with a base class property and which JavaScript cannot read or enumerate.

open Fickl.Core

[<PrivateFields>]
type Account(owner: string) =
    let mutable balance = 0.0
    member _.Owner = owner
    member _.Balance = balance
    member _.Deposit(amount: float) = balance <- balance + amount
import { class_type, string_type } from "@fable-org/fable-library-js/Reflection.js";

export class Account {
    #owner;
    #balance;
    constructor(owner) {
        this.#owner = owner;
        this.#balance = 0;
    }
    get Owner() {
        const _ = this;
        return _.#owner;
    }
    get Balance() {
        const _ = this;
        return _.#balance;
    }
    Deposit(amount) {
        const _ = this;
        _.#balance = (_.#balance + amount);
    }
}

export function Account_$reflection() {
    return class_type("Example3.Account", undefined, Account, undefined, () => [[[["owner", string_type]]]]);
}

export function Account_$ctor_Z721C83C5(owner) {
    return new Account(owner);
}
import type { float64 } from "@fable-org/fable-library-ts/Int32.ts";
import { class_type, string_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";

export class Account {
    readonly #owner: string;
    #balance: float64;
    constructor(owner: string) {
        this.#owner = owner;
        this.#balance = 0;
    }
    get Owner(): string {
        const _: Account = this;
        return _.#owner;
    }
    get Balance(): float64 {
        const _: Account = this;
        return _.#balance;
    }
    Deposit(amount: float64): void {
        const _: Account = this;
        _.#balance = (_.#balance + amount);
    }
}

export function Account_$reflection(): TypeInfo {
    return class_type("Example3.Account", undefined, Account, undefined, () => [[[["owner", string_type]]]]);
}

export function Account_$ctor_Z721C83C5(owner: string): Account {
    return new Account(owner);
}

The members of the class are attached, because only the class body can read #private fields. A class that inherits a global or imported JavaScript class uses private fields without the attribute, and [<PrivateFields(false)>] turns them off.

The --privateFields flag does the same for every class with attached members, without marking each one.

open Fable.Core

[<AttachMembers>]
type Account(opening: float) =
    let mutable balance = opening
    member _.Balance = balance
    member _.Deposit(amount: float) = balance <- balance + amount
import { class_type, float64_type } from "@fable-org/fable-library-js/Reflection.js";

export class Account {
    #balance;
    constructor(opening) {
        this.#balance = opening;
    }
    get Balance() {
        const _ = this;
        return _.#balance;
    }
    Deposit(amount) {
        const _ = this;
        _.#balance = (_.#balance + amount);
    }
}

export function Account_$reflection() {
    return class_type("Example4.Account", undefined, Account, undefined, () => [[[["opening", float64_type]]]]);
}

export function Account_$ctor_5E38073B(opening) {
    return new Account(opening);
}
import type { float64 } from "@fable-org/fable-library-ts/Int32.ts";
import { class_type, float64_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";

export class Account {
    #balance: float64;
    constructor(opening: float64) {
        this.#balance = opening;
    }
    get Balance(): float64 {
        const _: Account = this;
        return _.#balance;
    }
    Deposit(amount: float64): void {
        const _: Account = this;
        _.#balance = (_.#balance + amount);
    }
}

export function Account_$reflection(): TypeInfo {
    return class_type("Example4.Account", undefined, Account, undefined, () => [[[["opening", float64_type]]]]);
}

export function Account_$ctor_5E38073B(opening: float64): Account {
    return new Account(opening);
}

See --privateFields for the flag on its own.

Decorators: [<JsDecorator>]

[<JsDecorator>] emits a native JavaScript decorator on a class, a method, a property or a field. The first argument is the decorator expression. With a second argument, a module path, the identifier the expression starts with is imported from that module; without one, the expression is a global. You can stack several on one member.

open Fickl.Core

[<JsDecorator("customElement('todo-list')", "lit/decorators.js")>]
type TodoList() =
    [<JsDecorator("property()", "lit/decorators.js")>]
    member val Title = "Todo" with get, set

    [<JsDecorator("eventOptions({ passive: true })", "lit/decorators.js")>]
    member _.OnScroll(_event: obj) = ()
import { customElement, eventOptions, property } from "lit/decorators.js";
import { class_type } from "@fable-org/fable-library-js/Reflection.js";

@(customElement('todo-list'))
export class TodoList {
    constructor() {
    }
    @(property())
    accessor Title = "Todo";
    @(eventOptions({ passive: true }))
    OnScroll(_event) {
    }
}

export function TodoList_$reflection() {
    return class_type("Example5.TodoList", undefined, TodoList);
}

export function TodoList_$ctor() {
    return new TodoList();
}
import { customElement, eventOptions, property } from "lit/decorators.js";
import { class_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";

@(customElement('todo-list'))
export class TodoList {
    constructor() {
    }
    @(property())
    accessor Title: string = "Todo";
    @(eventOptions({ passive: true }))
    OnScroll(_event: any): void {
    }
}

export function TodoList_$reflection(): TypeInfo {
    return class_type("Example5.TodoList", undefined, TodoList);
}

export function TodoList_$ctor(): TodoList {
    return new TodoList();
}

A decorated auto-property compiles to an accessor. Pass true as the last argument, JsDecorator("expr", true) or JsDecorator("expr", "module", true), to decorate a native field instead. A relative import path is relative to the declaring F# file. The decorators run in the tool that executes the output, so it must support standard decorators.

Symbol keys: [<SymbolKey>]

[<SymbolKey>] uses a symbol expression as the JavaScript key of a member, an interface slot or a record field, instead of its name. Use a well-known symbol such as Symbol.iterator or Symbol.toPrimitive, a stable global such as Symbol.for('app.state'), or an imported symbol, with the module path as the second argument.

open Fickl.Core

type ILabel =
    [<SymbolKey("Symbol.toStringTag")>]
    abstract Label: string

let create (label: string) : ILabel =
    { new ILabel with member _.Label = label }

type Money(cents: int) =
    [<SymbolKey("Symbol.toPrimitive")>]
    member _.ToPrimitive(_hint: string) = cents
import { class_type, int32_type } from "@fable-org/fable-library-js/Reflection.js";

export function create(label) {
    return {
        [Symbol.toStringTag]: label,
    };
}

export class Money {
    constructor(cents) {
        this.cents = (cents | 0);
    }
    [Symbol.toPrimitive](_hint) {
        const _ = this;
        return _.cents | 0;
    }
}

export function Money_$reflection() {
    return class_type("Example6.Money", undefined, Money, undefined, () => [[[["cents", int32_type]]]]);
}

export function Money_$ctor_Z524259A4(cents) {
    return new Money(cents);
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import { class_type, int32_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";

export interface ILabel {
    get [Symbol.toStringTag](): string
}

export function create(label: string): ILabel {
    return {
        [Symbol.toStringTag]: label,
    };
}

export class Money {
    readonly cents: int32;
    constructor(cents: int32) {
        this.cents = (cents | 0);
    }
    [Symbol.toPrimitive](_hint: string): int32 {
        const _: Money = this;
        return _.cents | 0;
    }
}

export function Money_$reflection(): TypeInfo {
    return class_type("Example6.Money", undefined, Money, undefined, () => [[[["cents", int32_type]]]]);
}

export function Money_$ctor_Z524259A4(cents: int32): Money {
    return new Money(cents);
}

A class member with a symbol key is attached to the class without [<AttachMembers>], so +money calls Money's [Symbol.toPrimitive]. The expression must give the same symbol at every access; Symbol('x') creates a new one each time.

new.target: JS.newTarget and JS.isNewTarget

JS.newTarget is JavaScript's new.target: the class being constructed, which is the subclass when a subclass constructor runs. JS.isNewTarget<'T> tests whether that class is 'T itself, new.target === Shape, which makes an abstract-base guard.

open Fickl.Core

type Shape() =
    do
        if JS.isNewTarget<Shape> then
            failwith "Shape is abstract"

type Square() =
    inherit Shape()

type Base() =
    let created: obj = JS.newTarget
    member _.Created = created
import { Exception } from "@fable-org/fable-library-js/Util.js";
import { class_type } from "@fable-org/fable-library-js/Reflection.js";

export class Shape {
    constructor() {
        if ((new.target) === Shape) {
            throw new Exception("Shape is abstract");
        }
    }
}

export function Shape_$reflection() {
    return class_type("Example7.Shape", undefined, Shape);
}

export function Shape_$ctor() {
    return new Shape();
}

export class Square extends Shape {
    constructor() {
        super();
    }
}

export function Square_$reflection() {
    return class_type("Example7.Square", undefined, Square, Shape_$reflection());
}

export function Square_$ctor() {
    return new Square();
}

export class Base {
    constructor() {
        this.created = (new.target);
    }
}

export function Base_$reflection() {
    return class_type("Example7.Base", undefined, Base);
}

export function Base_$ctor() {
    return new Base();
}

export function Base__get_Created(_) {
    return _.created;
}
import { Exception } from "@fable-org/fable-library-ts/Util.ts";
import { class_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";

export class Shape {
    constructor() {
        if ((new.target) === Shape) {
            throw new Exception("Shape is abstract");
        }
    }
}

export function Shape_$reflection(): TypeInfo {
    return class_type("Example7.Shape", undefined, Shape);
}

export function Shape_$ctor(): Shape {
    return new Shape();
}

export class Square extends Shape {
    constructor() {
        super();
    }
}

export function Square_$reflection(): TypeInfo {
    return class_type("Example7.Square", undefined, Square, Shape_$reflection());
}

export function Square_$ctor(): Square {
    return new Square();
}

export class Base {
    readonly created: any;
    constructor() {
        this.created = (new.target);
    }
}

export function Base_$reflection(): TypeInfo {
    return class_type("Example7.Base", undefined, Base);
}

export function Base_$ctor(): Base {
    return new Base();
}

export function Base__get_Created(_: Base): any {
    return _.created;
}

Both are allowed only in a primary constructor: its let and do bindings and the arguments of its inherit call. Inside a lambda or an object expression there, and anywhere else, they are a compile error. For JS.isNewTarget, 'T must be a class that compiles to a JavaScript class. On .NET both throw.

Predictable method names: [<NoOverloadSuffix>]

JavaScript has no overloading, so Fable names a class method after its type and adds a hash of its parameter types, Counter__Subtract_Z524259A4, even when the method is not overloaded. [<NoOverloadSuffix>] drops the hash, so JavaScript can call the method by a name you can predict, Counter__Add.

open Fickl.Core

type Counter(start: int) =
    [<NoOverloadSuffix>]
    member _.Add(x: int) = start + x

    member _.Subtract(x: int) = start - x
import { class_type, int32_type } from "@fable-org/fable-library-js/Reflection.js";

export class Counter {
    constructor(start) {
        this.start = (start | 0);
    }
}

export function Counter_$reflection() {
    return class_type("Example8.Counter", undefined, Counter, undefined, () => [[[["start", int32_type]]]]);
}

export function Counter_$ctor_Z524259A4(start) {
    return new Counter(start);
}

export function Counter__Add(_, x) {
    return (_.start + x) | 0;
}

export function Counter__Subtract_Z524259A4(_, x) {
    return (_.start - x) | 0;
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import { class_type, int32_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";

export class Counter {
    readonly start: int32;
    constructor(start: int32) {
        this.start = (start | 0);
    }
}

export function Counter_$reflection(): TypeInfo {
    return class_type("Example8.Counter", undefined, Counter, undefined, () => [[[["start", int32_type]]]]);
}

export function Counter_$ctor_Z524259A4(start: int32): Counter {
    return new Counter(start);
}

export function Counter__Add(_: Counter, x: int32): int32 {
    return (_.start + x) | 0;
}

export function Counter__Subtract_Z524259A4(_: Counter, x: int32): int32 {
    return (_.start - x) | 0;
}

On a type, the attribute drops the hash of every member that is not overloaded; overloaded members keep theirs. On an overloaded member it is an error. On a property, it applies to the getter and the setter.

Edit this page