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.