Skip to content

Types

Fickl.Core has types that exist mainly for their TypeScript annotation: property keys, plain-object records, branded primitives, the TypeScript utility types and unknown. Use them when TypeScript code calls your compiled F#, or when you bind a JavaScript API whose types F# cannot spell. Most of them compile to the bare value in JavaScript; only the annotations change.

Property keys: keyof

keyof takes a lambda that reads one property and gives you the name JavaScript reads that property by. The result is a keyof<'T, 'R>: a key of 'T whose value is an 'R. KeyOf.get and KeyOf.set read and write the property through the key, and key.Value is the name as a string. A keyof<'T> is a key whose value type you do not track; KeyOf.widen converts one to the other.

open Fickl.Core

type Server = { Port: int; Host: string; mutable Retries: int }

let port = keyof (fun (s: Server) -> s.Port)
let retries = keyof (fun (s: Server) -> s.Retries)

let read (key: keyof<Server, 'R>) (server: Server) : 'R = KeyOf.get key server
let name (key: keyof<Server>) = key.Value

let retry (server: Server) =
    KeyOf.set retries server (server.Retries + 1)
import { Record } from "@fable-org/fable-library-js/Types.js";
import { record_type, string_type, int32_type } from "@fable-org/fable-library-js/Reflection.js";

export class Server extends Record {
    constructor(Port, Host, Retries) {
        super();
        this.Port = (Port | 0);
        this.Host = Host;
        this.Retries = (Retries | 0);
    }
}

export function Server_$reflection() {
    return record_type("Example1.Server", [], Server, () => [["Port", int32_type], ["Host", string_type], ["Retries", int32_type]]);
}

export const port = "Port";

export const retries = "Retries";

export function read(key, server) {
    return server[key];
}

export function name(key) {
    return key;
}

export function retry(server) {
    server[retries] = ((server.Retries + 1) | 0);
}
import { Record } from "@fable-org/fable-library-ts/Types.ts";
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import type { KeysOfType, IComparable, IEquatable } from "@fable-org/fable-library-ts/Util.ts";
import { record_type, string_type, int32_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";

export class Server extends Record implements IEquatable<Server>, IComparable<Server> {
    readonly Port: int32;
    readonly Host: string;
    Retries: int32;
    constructor(Port: int32, Host: string, Retries: int32) {
        super();
        this.Port = (Port | 0);
        this.Host = Host;
        this.Retries = (Retries | 0);
    }
}

export function Server_$reflection(): TypeInfo {
    return record_type("Example1.Server", [], Server, () => [["Port", int32_type], ["Host", string_type], ["Retries", int32_type]]);
}

export const port = "Port";

export const retries = "Retries";

export function read<R>(key: KeysOfType<Server, R>, server: Server): R {
    return (server as globalThis.Record<KeysOfType<Server, R>, R>)[key];
}

export function name(key: (keyof Server) & string): string {
    return key;
}

export function retry(server: Server): void {
    server[retries] = ((server.Retries + 1) | 0);
}

In JavaScript a key is the string itself, const port = "Port", and KeyOf.get is server[key]. TypeScript annotates a keyof<Server> as (keyof Server) & string and a keyof<Server, 'R> as KeysOfType<Server, R>, the keys whose property is an R, so target[key] is checked. The name is the one the compiled access uses, so a renamed member gives its JavaScript name.

The lambda must read a single property of its argument. A path such as _.a.b, a method or a class property is a compile error. KeyOf.tryGet reads a property as an obj option, and KeyOf.unsafeOfString treats any string as a key, with a visible cast in TypeScript.

Plain-object maps: Record, ReadonlyRecord, PropertyRecord

Record<'K, 'V> is a plain JavaScript object used as a map, typed Record<K, V> in TypeScript. r.[key] compiles to r[key], and r.[key] <- value to r[key] = value. Record.empty () builds {}, Record.tryGet finds only the object's own properties, so an inherited toString is not found, and Record.keys is Object.keys.

ReadonlyRecord<'K, 'V> is the same object without the setter, typed Readonly<Record<K, V>>. PropertyRecord<'T, 'V> has a 'V for each property name of 'T, typed Record<keyof T & string, V>, and you index it with a keyof<'T>.

open Fickl.Core

let count (words: string[]) : Record<string, int> =
    let counts = Record.empty ()

    for word in words do
        counts.[word] <- (Record.tryGet word counts |> Option.defaultValue 0) + 1

    counts

let total (counts: ReadonlyRecord<string, int>) (word: string) = counts.[word]

type Theme = { Primary: string; Accent: string }

let primary (colors: PropertyRecord<Theme, string>) =
    colors.[KeyOf.widen (keyof (fun (t: Theme) -> t.Primary))]
import { item } from "@fable-org/fable-library-js/Array.js";
import { defaultArg } from "@fable-org/fable-library-js/Option.js";
import { Record } from "@fable-org/fable-library-js/Types.js";
import { record_type, string_type } from "@fable-org/fable-library-js/Reflection.js";

export function count(words) {
    const counts = {};
    for (let idx = 0; idx <= (words.length - 1); idx++) {
        const word = words[idx] ?? item(idx, words);
        counts[word]=(defaultArg(Object.hasOwn(counts, word) ? counts[word] : undefined, 0) + 1);
    }
    return counts;
}

export function total(counts, word) {
    return (counts[word]) | 0;
}

export class Theme extends Record {
    constructor(Primary, Accent) {
        super();
        this.Primary = Primary;
        this.Accent = Accent;
    }
}

export function Theme_$reflection() {
    return record_type("Example2.Theme", [], Theme, () => [["Primary", string_type], ["Accent", string_type]]);
}

export function primary(colors) {
    return colors["Primary"];
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import { item } from "@fable-org/fable-library-ts/Array.ts";
import { defaultArg } from "@fable-org/fable-library-ts/Option.ts";
import type { IComparable, IEquatable, MutableArray } from "@fable-org/fable-library-ts/Util.ts";
import { Record } from "@fable-org/fable-library-ts/Types.ts";
import { record_type, string_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";

export function count(words: MutableArray<string>): globalThis.Record<string, int32> {
    const counts = {} as globalThis.Record<string, int32>;
    for (let idx = 0; idx <= (words.length - 1); idx++) {
        const word: string = words[idx] ?? item<string>(idx, words);
        counts[word]=(defaultArg(Object.hasOwn(counts, word) ? counts[word] : undefined, 0) + 1);
    }
    return counts;
}

export function total(counts: Readonly<globalThis.Record<string, int32>>, word: string): int32 {
    return (counts[word]) | 0;
}

export class Theme extends Record implements IEquatable<Theme>, IComparable<Theme> {
    readonly Primary: string;
    readonly Accent: string;
    constructor(Primary: string, Accent: string) {
        super();
        this.Primary = Primary;
        this.Accent = Accent;
    }
}

export function Theme_$reflection(): TypeInfo {
    return record_type("Example2.Theme", [], Theme, () => [["Primary", string_type], ["Accent", string_type]]);
}

export function primary(colors: globalThis.Record<keyof Theme & string, string>): string {
    return colors[("Primary" as (keyof Theme) & string)];
}

TypeScript annotations write globalThis.Record, so the Record class of the Fable library cannot shadow the TypeScript type. A missing key reads as undefined at runtime, as it does in JavaScript; use Record.tryGet when the key may be absent.

Property values: valueof

valueof<'T> is the value of some property of 'T, without saying which: TypeScript's T[keyof T]. ValueOf.ofKey key target reads one, and ValueOf.unsafeAs treats it as a given type. Nothing checks that cast, and TypeScript shows it as an as.

open Fickl.Core

type Server = { Port: int; Host: string }

let valueAt (key: keyof<Server, 'R>) (server: Server) : valueof<Server> =
    ValueOf.ofKey key server

let portOf (server: Server) : int =
    valueAt (keyof (fun (s: Server) -> s.Port)) server |> ValueOf.unsafeAs<Server, int>
import { Record } from "@fable-org/fable-library-js/Types.js";
import { record_type, string_type, int32_type } from "@fable-org/fable-library-js/Reflection.js";

export class Server extends Record {
    constructor(Port, Host) {
        super();
        this.Port = (Port | 0);
        this.Host = Host;
    }
}

export function Server_$reflection() {
    return record_type("Example3.Server", [], Server, () => [["Port", int32_type], ["Host", string_type]]);
}

export function valueAt(key, server) {
    return server[key];
}

export function portOf(server) {
    return valueAt("Port", server) | 0;
}
import { Record } from "@fable-org/fable-library-ts/Types.ts";
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import type { KeysOfType, IComparable, IEquatable } from "@fable-org/fable-library-ts/Util.ts";
import { record_type, string_type, int32_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";

export class Server extends Record implements IEquatable<Server>, IComparable<Server> {
    readonly Port: int32;
    readonly Host: string;
    constructor(Port: int32, Host: string) {
        super();
        this.Port = (Port | 0);
        this.Host = Host;
    }
}

export function Server_$reflection(): TypeInfo {
    return record_type("Example3.Server", [], Server, () => [["Port", int32_type], ["Host", string_type]]);
}

export function valueAt<R>(key: KeysOfType<Server, R>, server: Server): Server[keyof Server] {
    return (server as globalThis.Record<KeysOfType<Server, R>, R>)[key] as Server[keyof Server];
}

export function portOf(server: Server): int32 {
    return (valueAt<int32>("Port", server) as int32) | 0;
}

The TypeScript signature of valueAt returns Server[keyof Server]. The JavaScript value is the property's value itself.

Branded primitives: string<'m>, bool<'m>, char<'m>

F# already lets you tag a number with a unit of measure, int<UserId>. Fickl.Core adds the same for string, bool and char, so string<UserId> and string<OrderId> are different types in F# even though both are strings. Brand.tag and Brand.untag convert a string, and Brand.tagBool, Brand.untagBool, Brand.tagChar and Brand.untagChar convert the others.

open Fickl.Core

[<Measure>]
type UserId

[<Measure>]
type OrderId

let userId (raw: string) : string<UserId> = Brand.tag raw

let describe (id: string<UserId>) = "user " + Brand.untag id

// describe (Brand.tag<OrderId> "o1") does not compile
export function userId(raw) {
    return raw;
}

export function describe(id) {
    return "user " + id;
}
export function userId(raw: string): string {
    return raw;
}

export function describe(id: string): string {
    return "user " + id;
}

The brand exists only for the F# type checker. The Brand functions compile to nothing, the value is the bare primitive in JavaScript, and TypeScript annotations write string, boolean and string.

Utility types: Partial, Required, Readonly

Partial<'T>, Required<'T> and Readonly<'T> are TypeScript's utility types, for binding signatures such as abstract setState: Partial<State> -> unit. They change only the annotation; the JavaScript value is the object itself.

partial builds an object with some of a type's properties, from a lambda that sets them as jsOptions does. TypeScript checks the object literal with satisfies Partial<T>, so a misspelled key or a mistyped value is an error there. full treats a Partial<'T> as the complete 'T, with a cast. Both live in Fickl.Core.JsInterop.

open Fickl.Core
open Fickl.Core.JsInterop

type State =
    abstract count: int with get, set
    abstract name: string with get, set

let rename (name: string) : Partial<State> = partial (fun s -> s.name <- name)

let complete (patch: Partial<State>) : State = full patch

let snapshot (state: State) : Readonly<State> = unbox state
export function rename(name) {
    return {
        name: name,
    };
}

export function complete(patch) {
    return patch;
}

export function snapshot(state) {
    return state;
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";

export interface State {
    get count(): int32,
    get name(): string,
    set count(arg0: int32),
    set name(arg0: string)
}

export function rename(name: string): Partial<State> {
    return {
        name: name,
    } satisfies Partial<State>;
}

export function complete(patch: Partial<State>): State {
    return patch as State;
}

export function snapshot(state: State): Readonly<State> {
    return state as Readonly<State>;
}

In TypeScript, jsOptions also checks its flat object literal with satisfies Partial<T> before casting it to the full type.

unknown

Fickl.Core.unknown is obj in F#, so the two convert freely. A module function or abstract member that takes or returns unknown has unknown in its TypeScript signature, so TypeScript callers must narrow the value before they use it. obj stays any.

open Fickl.Core

let parse (text: string) : unknown = Fable.Core.JS.JSON.parse text

let anything (value: obj) : obj = value

type IStore =
    abstract Get: key: string -> unknown
export function parse(text) {
    return JSON.parse(text);
}

export function anything(value) {
    return value;
}
export function parse(text: string): unknown;

export function parse(text: string): any {
    return JSON.parse(text);
}

export function anything(value: any): any {
    return value;
}

export interface IStore {
    Get(key: string): unknown
}

The function body still sees any: the TypeScript output declares parse(text: string): unknown and then an implementation signature returning any. Values, fields and properties typed unknown are annotated any. The JavaScript is unchanged.

Your own annotation: [<EmitType>]

[<EmitType>] writes the TypeScript annotation of a type as the text you give, for types F# cannot describe: conditional, mapped and template literal types, or a library's helper type. $0, $1... in the text stand for the annotations of the type's generic arguments. With a second argument, a module path, the identifier the text starts with is imported from that module as a type.

open Fable.Core
open Fickl.Core

[<EmitType("`data-${string}`"); Erase>]
type DataAttr = DataAttr of string

[<EmitType("Record<string, $0>")>]
type Dict<'T> = interface end

[<EmitType("EventEmitter", "node:events")>]
type Emitter = interface end

let dataAttr (name: string) = DataAttr("data-" + name)
let lookup (dict: Dict<int>) (key: string) : int = unbox (JsInterop.emitJsExpr (dict, key) "$0[$1]")
let listen (emitter: Emitter) = emitter
export function dataAttr(name) {
    return "data-" + name;
}

export function lookup(dict, key) {
    return (dict[key]) | 0;
}

export function listen(emitter) {
    return emitter;
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import type { EventEmitter } from "node:events";

export function dataAttr(name: string): `data-${string}` {
    return "data-" + name as `data-${string}`;
}

export function lookup(dict: Record<string, int32>, key: string): int32 {
    return (dict[key]) | 0;
}

export function listen(emitter: EventEmitter): EventEmitter {
    return emitter;
}

The attribute changes only the TypeScript annotations; the JavaScript is the same with or without it. An interface or an erased union marked [<EmitType>] has no TypeScript declaration of its own, and a class keeps its JavaScript class. A relative module path is relative to the declaring F# file.

Enums: [<TsEnum>] and imported [<StringEnum>] unions

Without an attribute, an F# enum emits nothing and is typed as its underlying number. [<TsEnum>] emits the enum as the frozen object a TypeScript enum compiles to, with its members and the reverse mapping, so JavaScript can read Color.Red and Color[0]. TypeScript types it with an ambient declare enum, which keeps the output erasable, and annotates values of the enum Color instead of int32.

A [<StringEnum>] union marked [<Import>] or [<Global>] binds a TypeScript string enum. Its cases compile to members of the imported object, Direction.Up, instead of their string literals, so they type-check against the enum.

open Fable.Core
open Fickl.Core

[<TsEnum>]
type Color =
    | Red = 0
    | Green = 1

let isRed (color: Color) = color = Color.Red

[<Import("Direction", "compass"); StringEnum>]
type Direction =
    | [<CompiledName("UP")>] Up
    | [<CompiledName("DOWN")>] Down

let opposite (dir: Direction) =
    match dir with
    | Up -> Down
    | Down -> Up
import { Direction } from "compass";

export const Color = Object.freeze({
    Red: 0,
    Green: 1,
    0: "Red",
    1: "Green",
});

export function isRed(color) {
    return color === 0;
}

export function opposite(dir) {
    if (dir === Direction.Down) {
        return Direction.Up;
    }
    else {
        return Direction.Down;
    }
}
import { Direction } from "compass";

declare enum Color_$enum {
    Red = 0,
    Green = 1
}

export const Color: typeof Color_$enum = Object.freeze({
    Red: 0,
    Green: 1,
    0: "Red",
    1: "Green",
}) as typeof Color_$enum;

export type Color = Color_$enum;

export function isRed(color: Color): boolean {
    return color === 0;
}

export function opposite(dir: Direction): Direction {
    if (dir === Direction.Down) {
        return Direction.Up;
    }
    else {
        return Direction.Down;
    }
}

F# code is unchanged: values of a [<TsEnum>] enum stay plain numbers, so isRed compares with 0. A [<Flags>] enum is typed number, as its values combine members. An enum with a 64-bit underlying type is not supported.

Read-only collections

Fable annotates IReadOnlyCollection<T>, IReadOnlyList<T> and IReadOnlyDictionary<K, V> as any. Fickl annotates the first two Iterable<T> and the dictionary IReadOnlyMap<K, V>, so TypeScript can pass a readonly T[] or a ReadonlyMap.

For a parameter that must be an array, Fickl.Core.JS.ReadonlyArray<'T> binds TypeScript's ReadonlyArray<T>. It accepts a readonly T[] and an as const array, which a 'T[] parameter rejects. JS.ReadonlyArray.ofArray and JS.ReadonlyArray.ofResizeArray view an F# array or ResizeArray without a copy, and JS.ReadonlyArray.toArray copies one back.

open System.Collections.Generic
open Fickl.Core

let size (xs: IReadOnlyCollection<int>) = xs.Count
let first (xs: IReadOnlyList<string>) = xs.[0]
let find (d: IReadOnlyDictionary<string, int>) (key: string) = d.[key]

let sum (xs: JS.ReadonlyArray<int>) = Seq.sum xs
let total = sum (JS.ReadonlyArray.ofArray [| 1; 2; 3 |])
import { item, count } from "@fable-org/fable-library-js/CollectionUtil.js";
import { getItemFromDict } from "@fable-org/fable-library-js/MapUtil.js";
import { sum as sum_1 } from "@fable-org/fable-library-js/Seq.js";

export function size(xs) {
    return count(xs) | 0;
}

export function first(xs) {
    return item(xs, 0);
}

export function find(d, key) {
    return getItemFromDict(d, key) | 0;
}

export function sum(xs) {
    return sum_1(xs, {
        GetZero: () => 0,
        Add: (x, y) => ((x + y) | 0),
    }) | 0;
}

export const total = sum(new Int32Array([1, 2, 3]));
import { item, count } from "@fable-org/fable-library-ts/CollectionUtil.ts";
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import { getItemFromDict } from "@fable-org/fable-library-ts/MapUtil.ts";
import type { IReadOnlyMap } from "@fable-org/fable-library-ts/Util.ts";
import { sum as sum_1 } from "@fable-org/fable-library-ts/Seq.ts";

export function size(xs: Iterable<int32>): int32 {
    return count(xs) | 0;
}

export function first(xs: Iterable<string>): string {
    return item(xs, 0);
}

export function find(d: IReadOnlyMap<string, int32>, key: string): int32 {
    return getItemFromDict(d, key) | 0;
}

export function sum(xs: ReadonlyArray<int32>): int32 {
    return sum_1<int32>(xs as Iterable<int32>, {
        GetZero: (): int32 => 0,
        Add: (x: int32, y: int32): int32 => ((x + y) | 0),
    }) | 0;
}

export const total: int32 = sum(new Int32Array([1, 2, 3]) as unknown as ReadonlyArray<int32>);

ReadonlyArray has only the members an F# array has whatever its representation. A numeric F# array is a typed array in JavaScript, which has no concat and no map to another element type, so use the Seq functions for those.

Nullable reference types

With <Nullable>enable</Nullable> in your project, an F# 9 nullable reference type such as string | null is annotated string | null in TypeScript, so TypeScript code can pass and receive null. Fable erases the | null and writes string.

let greet (name: string | null) =
    match name with
    | null -> "Hello"
    | name -> "Hello " + name

let tryFind (key: string) : string | null =
    if key = "" then null else key
export function greet(name) {
    if (name == null) {
        return "Hello";
    }
    else {
        return "Hello " + name;
    }
}

export function tryFind(key) {
    if (key === "") {
        return null;
    }
    else {
        return key;
    }
}
export function greet(name: string | null): string {
    if (name == null) {
        return "Hello";
    }
    else {
        return "Hello " + name;
    }
}

export function tryFind(key: string): string | null {
    if (key === "") {
        return null;
    }
    else {
        return key;
    }
}

Joining strings that may be null

In .NET, + treats a null string as empty, so "a" + null + "b" is "ab". JavaScript's + writes the word null. Fickl can only make up the difference where the compiler knows that a string may be null:

The operandOutput of "a" + s + "b" when s is null
Typed string \| null, with <Nullable>enable</Nullable>"ab", as in .NET: the operand compiles to (s \|\| "")
Typed string, with or without the flag"anullb"

The second row covers a string that is null at run time: let s: string = null with nullness off, Unchecked.defaultof<string>, or a value from JavaScript or a deserialiser. Guarding every string operand was tried and dropped: it adds code to every concatenation, and TypeScript rejects "bar" || "" as a condition that is always true.

String.Concat, interpolated strings, sprintf "%s" and string give "" for a null string with or without the flag, so use one of them where a string may be null.

let label (title: string) (name: string | null) = title + " " + name + "!"
let safe (s: string) = $"[{s}]"
import { concat } from "@fable-org/fable-library-js/String.js";

export function label(title, name) {
    return ((title + " ") + (name || "")) + "!";
}

export function safe(s) {
    return concat("[", s, "]");
}
import { concat } from "@fable-org/fable-library-ts/String.ts";

export function label(title: string, name: string | null): string {
    return ((title + " ") + (name || "")) + "!";
}

export function safe(s: string): string {
    return concat("[", s, "]");
}
Edit this page