Skip to content

Reflection

Fable compiles a subset of .NET reflection: typeof, FSharpType and FSharpValue over records, unions and tuples, and the names and generic arguments of types. Fickl adds methods, constructors and union case attributes. This page covers what each addition supports and where it stops, so you can tell before you write the code whether it compiles.

JavaScript has no metadata tables to look members up in, so each addition works in one of two ways: resolved while compiling, which needs the type and the name in the source, or carried in the type's reflection info, which costs bytes in the output.

Type.GetMethod and MethodInfo.Invoke

typeof<T>.GetMethod("Name"), with a type known at compile time and a literal name, is resolved while compiling. It gives a MethodInfo that carries a call to that one member, so Invoke, Name, GetParameters, ReturnType and IsStatic work (#293).

type Calculator(offset: int) =
    member _.Add(a: int, b: int) = a + b + offset
    static member Twice(x: int) = x * 2

let twice = typeof<Calculator>.GetMethod("Twice") |> nonNull

let callTwice () = twice.Invoke(null, [| box 21 |]) :?> int

let callAdd () =
    match typeof<Calculator>.GetMethod("Add") with
    | null -> 0
    | add -> add.Invoke(Calculator(1), [| box 2; box 3 |]) :?> int

let describe () =
    let parameters = twice.GetParameters() |> Array.map (fun p -> p.Name)
    parameters, twice.ReturnType.Name, twice.IsStatic
import { isStatic, name, invoke, MethodInfo, class_type, int32_type } from "@fable-org/fable-library-js/Reflection.js";
import { Operators_NonNull } from "@fable-org/fable-library-js/FSharp.Core.js";
import { map } from "@fable-org/fable-library-js/Array.js";

export class Calculator {
    constructor(offset) {
        this.offset = (offset | 0);
    }
}

export function Calculator_$reflection() {
    return class_type("Example1.Calculator", undefined, Calculator, undefined, () => [[[["offset", int32_type]]]]);
}

export function Calculator_$ctor_Z524259A4(offset) {
    return new Calculator(offset);
}

export function Calculator__Add_Z37302880(_, a, b) {
    return ((a + b) + _.offset) | 0;
}

export function Calculator_Twice_Z524259A4(x) {
    return (x * 2) | 0;
}

export const twice = Operators_NonNull(new MethodInfo("Twice", [["x", int32_type]], int32_type, true, (target, args) => (Calculator_Twice_Z524259A4(args[0]) | 0)));

export function callTwice() {
    return invoke(twice, null, [21]) | 0;
}

export function callAdd() {
    const matchValue = new MethodInfo("Add", [["a", int32_type], ["b", int32_type]], int32_type, false, (target, args) => (Calculator__Add_Z37302880(target, args[0], args[1]) | 0));
    if (matchValue == null) {
        return 0;
    }
    else {
        return invoke(matchValue, Calculator_$ctor_Z524259A4(1), [2, 3]) | 0;
    }
}

export function describe() {
    return [map(name, twice.parameters), name(twice.returnType), isStatic(twice)];
}
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import { isStatic, name, invoke, MethodInfo, class_type, int32_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";
import { Operators_NonNull } from "@fable-org/fable-library-ts/FSharp.Core.ts";
import { map } from "@fable-org/fable-library-ts/Array.ts";
import type { MutableArray } from "@fable-org/fable-library-ts/Util.ts";

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

export function Calculator_$reflection(): TypeInfo {
    return class_type("Example1.Calculator", undefined, Calculator, undefined, () => [[[["offset", int32_type]]]]);
}

export function Calculator_$ctor_Z524259A4(offset: int32): Calculator {
    return new Calculator(offset);
}

export function Calculator__Add_Z37302880(_: Calculator, a: int32, b: int32): int32 {
    return ((a + b) + _.offset) | 0;
}

export function Calculator_Twice_Z524259A4(x: int32): int32 {
    return (x * 2) | 0;
}

export const twice: any = Operators_NonNull<any>(new MethodInfo("Twice", [["x", int32_type] as [string, any]], int32_type, true, (target: any, args: any[]): int32 => (Calculator_Twice_Z524259A4(args[0]) | 0)));

export function callTwice(): int32 {
    return (invoke(twice, null, [21]) as int32) | 0;
}

export function callAdd(): int32 {
    const matchValue: any | null = new MethodInfo("Add", [["a", int32_type] as [string, any], ["b", int32_type] as [string, any]], int32_type, false, (target: any, args: any[]): int32 => (Calculator__Add_Z37302880(target, args[0], args[1]) | 0));
    if (matchValue == null) {
        return 0;
    }
    else {
        return (invoke(matchValue, Calculator_$ctor_Z524259A4(1), [2, 3]) as int32) | 0;
    }
}

export function describe(): [MutableArray<string | null>, string, boolean] {
    return [map<any, string | null>(name, twice.parameters), name(twice.returnType), isStatic(twice)] as [MutableArray<string | null>, string, boolean];
}

Each GetMethod call compiles to a new MethodInfo(...) with the method's name, parameters and return type, and a function that calls the compiled member. A missing or private method gives null, as on .NET. With <Nullable>enable</Nullable>, F# types the result as nullable, so match on null or use nonNull.

The type must be a class, record, union or interface compiled from F# sources. These are compile errors:

  • a name that is not a literal;
  • a type that is not known at compile time, including the result of MakeGenericType; typeof<Box<int>>.GetMethod(...) works;
  • a name with more than one public method: pass the parameter types, GetMethod("Add", [| typeof<int>; typeof<int> |]);
  • a generic method, or an inline member;
  • binding flags other than Public, Instance and Static; Public is required.

An exception thrown by the invoked method propagates as it is, where .NET wraps it in a TargetInvocationException. GetMethods and GetProperty(name).GetValue are not implemented.

Constructors and Type.IsClass

Type.GetConstructors(), GetConstructor(Type[]), ConstructorInfo.GetParameters, Invoke and Name, ParameterInfo.Position, and Type.IsClass compile for JavaScript and TypeScript (#277). Unlike GetMethod, these work on a Type that is only known at run time, such as p.ParameterType.GetConstructors().

type Point(x: int, y: int) =
    new() = Point(0, 0)
    member _.X = x
    member _.Y = y

let signatures () =
    typeof<Point>.GetConstructors()
    |> Array.map (fun ctor ->
        ctor.GetParameters() |> Array.map (fun p -> $"{p.Position}:{p.Name}"))

let make () =
    let ctor = typeof<Point>.GetConstructor([| typeof<int>; typeof<int> |]) |> nonNull
    ctor.Invoke([| box 1; box 2 |]) :?> Point

let isClass = typeof<Point>.IsClass
import { getConstructor, getConstructors, name, class_type, int32_type } from "@fable-org/fable-library-js/Reflection.js";
import { map } from "@fable-org/fable-library-js/Array.js";
import { Operators_NonNull } from "@fable-org/fable-library-js/FSharp.Core.js";

export class Point {
    constructor(x, y) {
        this.x = (x | 0);
        this.y = (y | 0);
    }
}

export function Point_$reflection() {
    return class_type("Example2.Point", undefined, Point, undefined, () => [[[["x", int32_type], ["y", int32_type]]], [[], Point_$ctor]]);
}

export function Point_$ctor_Z37302880(x, y) {
    return new Point(x, y);
}

export function Point_$ctor() {
    return Point_$ctor_Z37302880(0, 0);
}

export function Point__get_X(_) {
    return _.x | 0;
}

export function Point__get_Y(_) {
    return _.y | 0;
}

export function signatures() {
    return map((ctor) => map((p) => (`${p[2]}:${name(p)}`), ctor.parameters), getConstructors(Point_$reflection()));
}

export function make() {
    const ctor = Operators_NonNull(getConstructor(Point_$reflection(), [int32_type, int32_type]));
    return ctor.invoke([1, 2]);
}

export const isClass = true;
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";
import { getConstructor, getConstructors, name, class_type, int32_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";
import { map } from "@fable-org/fable-library-ts/Array.ts";
import type { MutableArray } from "@fable-org/fable-library-ts/Util.ts";
import { Operators_NonNull } from "@fable-org/fable-library-ts/FSharp.Core.ts";

export class Point {
    readonly y: int32;
    readonly x: int32;
    constructor(x: int32, y: int32) {
        this.x = (x | 0);
        this.y = (y | 0);
    }
}

export function Point_$reflection(): TypeInfo {
    return class_type("Example2.Point", undefined, Point, undefined, () => [[[["x", int32_type], ["y", int32_type]]], [[], Point_$ctor]]);
}

export function Point_$ctor_Z37302880(x: int32, y: int32): Point {
    return new Point(x, y);
}

export function Point_$ctor(): Point {
    return Point_$ctor_Z37302880(0, 0);
}

export function Point__get_X(_: Point): int32 {
    return _.x | 0;
}

export function Point__get_Y(_: Point): int32 {
    return _.y | 0;
}

export function signatures(): MutableArray<MutableArray<string>> {
    return map<any, MutableArray<string>>((ctor: any): MutableArray<string> => map<any, string>((p: any): string => (`${p[2]}:${name(p)}`), ctor.parameters), getConstructors(Point_$reflection()));
}

export function make(): Point {
    const ctor: any = Operators_NonNull<any>(getConstructor(Point_$reflection(), [int32_type, int32_type]));
    return ctor.invoke([1, 2]) as Point;
}

export const isClass = true;

The constructors are listed in declaration order, in the class's reflection info: Point_$reflection gains [[[["x", int32_type], ["y", int32_type]]], [[], Point_$ctor]]. GetConstructor returns null when no constructor has the given parameter types. Records and anonymous records report their constructor too; unions, interfaces and enums report none. IsClass of a type known at compile time is a constant, export const isClass = true; here.

Because any Type can reach GetConstructors at run time, the compiler cannot tell which classes need the information, so every class carries it:

  • A class whose public constructors are not a single parameterless one carries about 25 bytes, plus about 20 per constructor parameter. A class with no constructor, only static members, carries () => [].
  • A class with a single parameterless constructor, records and unions carry nothing extra.
  • The JavaScript runtime library grew by about 1.4%.
  • Nothing is emitted under --noReflection. The information sits inside the type's $reflection function, so a bundler that drops unused reflection removes it with it.

The limits:

  • Types the runtime represents natively, such as tuples, options, lists, strings, arrays and functions, report no constructors.
  • At run time, IsClass is true for struct tuples, voption and Nullable<T>, which cannot be told apart from their reference counterparts.
  • An F# exception reports one constructor, where .NET reports two.
  • Invoking the constructor of an abstract class constructs an object, where .NET throws.

UnionCaseInfo.GetCustomAttributes

GetCustomAttributes() and GetCustomAttributes(typeof<T>) work on union cases, for attributes defined in your project (#278).

open Microsoft.FSharp.Reflection

type LabelAttribute(text: string) =
    inherit System.Attribute()
    member _.Text = text

type Status =
    | [<Label("Waiting")>] Pending
    | [<Label("Done")>] Complete

let labels () =
    FSharpType.GetUnionCases(typeof<Status>)
    |> Array.map (fun case ->
        match case.GetCustomAttributes(typeof<LabelAttribute>) with
        | [| :? LabelAttribute as label |] -> label.Text
        | _ -> case.Name)
import { Union, Attribute } from "@fable-org/fable-library-js/Types.js";
import { getUnionCases, name, getCaseCustomAttributes, union_type, class_type, string_type } from "@fable-org/fable-library-js/Reflection.js";
import { item, equalsWith, map } from "@fable-org/fable-library-js/Array.js";
import { defaultOf, equals } from "@fable-org/fable-library-js/Util.js";

export class LabelAttribute extends Attribute {
    constructor(text) {
        super();
        this.text = text;
    }
}

export function LabelAttribute_$reflection() {
    return class_type("Example3.LabelAttribute", undefined, LabelAttribute, class_type("System.Attribute"), () => [[[["text", string_type]]]]);
}

export function LabelAttribute_$ctor_Z721C83C5(text) {
    return new LabelAttribute(text);
}

export function LabelAttribute__get_Text(_) {
    return _.text;
}

export class Status extends Union {
    constructor(tag, fields) {
        super();
        this.tag = tag;
        this.fields = fields;
    }
    cases() {
        return ["Pending", "Complete"];
    }
    static Pending = new Status(0, []);
    static Complete = new Status(1, []);
}

export function Status_$reflection() {
    return union_type("Example3.Status", [], Status, () => [[], []], Status_$caseAttributes);
}

export function Status_$caseAttributes() {
    return [[LabelAttribute_$ctor_Z721C83C5("Waiting")], [LabelAttribute_$ctor_Z721C83C5("Done")]];
}

export function labels() {
    return map((case$) => {
        const matchValue = getCaseCustomAttributes(case$, LabelAttribute_$reflection());
        let matchResult, label;
        if (!equalsWith(equals, matchValue, defaultOf()) && (matchValue.length === 1)) {
            if ((matchValue[0] ?? item(0, matchValue)) instanceof LabelAttribute) {
                matchResult = 0;
                label = (matchValue[0] ?? item(0, matchValue));
            }
            else {
                matchResult = 1;
            }
        }
        else {
            matchResult = 1;
        }
        switch (matchResult) {
            case 0:
                return LabelAttribute__get_Text(label);
            default:
                return name(case$);
        }
    }, getUnionCases(Status_$reflection()));
}
import { Union, Attribute } from "@fable-org/fable-library-ts/Types.ts";
import { getUnionCases, name, getCaseCustomAttributes, union_type, class_type, string_type, type TypeInfo } from "@fable-org/fable-library-ts/Reflection.ts";
import { defaultOf, equals, type MutableArray } from "@fable-org/fable-library-ts/Util.ts";
import { item, equalsWith, map } from "@fable-org/fable-library-ts/Array.ts";
import type { int32 } from "@fable-org/fable-library-ts/Int32.ts";

export class LabelAttribute extends Attribute {
    readonly text: string;
    constructor(text: string) {
        super();
        this.text = text;
    }
}

export function LabelAttribute_$reflection(): TypeInfo {
    return class_type("Example3.LabelAttribute", undefined, LabelAttribute, class_type("System.Attribute"), () => [[[["text", string_type]]]]);
}

export function LabelAttribute_$ctor_Z721C83C5(text: string): LabelAttribute {
    return new LabelAttribute(text);
}

export function LabelAttribute__get_Text(_: LabelAttribute): string {
    return _.text;
}

export type Status_$union = 
    | Status<0>
    | Status<1>

export type Status_$cases = {
    0: ["Pending", []],
    1: ["Complete", []]
}

export function Status_Pending() {
    return Status.Pending;
}

export function Status_Complete() {
    return Status.Complete;
}

export class Status<Tag extends keyof Status_$cases> extends Union<Tag, Status_$cases[Tag][0]> {
    constructor(tag: Tag, fields: Status_$cases[Tag][1]) {
        super();
        this.tag = tag;
        this.fields = fields;
    }
    readonly tag: Tag;
    readonly fields: Status_$cases[Tag][1];
    cases() {
        return ["Pending", "Complete"];
    }
    static readonly Pending: Status_$union = new Status<0>(0, []);
    static readonly Complete: Status_$union = new Status<1>(1, []);
}

export function Status_$reflection(): TypeInfo {
    return union_type("Example3.Status", [], Status, () => [[], []], Status_$caseAttributes);
}

export function Status_$caseAttributes(): MutableArray<any[]> {
    return [[LabelAttribute_$ctor_Z721C83C5("Waiting")], [LabelAttribute_$ctor_Z721C83C5("Done")]];
}

export function labels(): MutableArray<string> {
    return map<any, string>((case$: any): string => {
        const matchValue: any[] = getCaseCustomAttributes(case$, LabelAttribute_$reflection());
        let matchResult: int32 = (undefined as any), label: LabelAttribute = (undefined as any);
        if (!equalsWith(equals, matchValue, defaultOf()) && (matchValue.length === 1)) {
            if ((matchValue[0] ?? item<any>(0, matchValue)) instanceof LabelAttribute) {
                matchResult = 0;
                label = (matchValue[0] ?? item<any>(0, matchValue) as LabelAttribute);
            }
            else {
                matchResult = 1;
            }
        }
        else {
            matchResult = 1;
        }
        switch (matchResult) {
            case 0:
                return LabelAttribute__get_Text(label!);
            default:
                return name(case$);
        }
    }, getUnionCases(Status_$reflection()));
}

The union's reflection info gains a Status_$caseAttributes function that constructs the attributes of each case, with their arguments. A union without such attributes emits nothing extra.

The parameterless call returns only the attributes that can be constructed, where .NET also returns compiler-generated and BCL ones. Only union cases are covered: attributes on types, record fields and properties are still not supported.

Edit this page