Bindings
Achieving parity with Type-Script in type-safety is certainly a goal.
Electron generates their .d.ts
from their docs directly.
The docs are parsed using @electron/docs-parser
A json api is generated.
The json api is then utilised by a separate package to generate the type definitions
Our parser uses the same source of truth to generate our bindings.
We first parse the json into strong types based off the source
We then map this to a more idiomatic FSharp representation.
We can then either choose to further map this to an intermediary AST such as Glutinum
to generate the source, or generate our own source using Fantomas
Parser Types The types are crafted from the @electron/docs-parser/ParsedDocumentation.d.ts
file which you can download ParsedDocumentation.d.ts to view.
My methodology to craft the types is to use Discriminated Unions to distinguish between types delineated by a tagged field such as type
.
export declare type DetailedStringType = {
type: 'String';
possibleValues: PossibleStringValue[] | null;
};
export declare type DetailedObjectType = {
type: 'Object';
properties: PropertyDocumentationBlock[];
};
export declare type DetailedEventType = {
type: 'Event';
eventProperties: PropertyDocumentationBlock[];
};
export declare type DetailedEventReferenceType = {
type: 'Event';
eventPropertiesReference: TypeInformation;
};
export declare type DetailedFunctionType = {
type: 'Function';
parameters: MethodParameterDocumentation[];
returns: TypeInformation | null;
};
export declare type DetailedType = ({
type: TypeInformation[];
} | DetailedFunctionType | DetailedObjectType | DetailedEventType | DetailedEventReferenceType | DetailedStringType | {
type: string;
}) & {
innerTypes?: TypeInformation[];
};
[<RequireQualifiedAccess>]
module TypeInformationKind =
type InfoArray = {
Collection: bool
Type: TypeInformation[]
}
type InfoString = {
Collection: bool
Type: string
}
type String = {
Collection: bool
PossibleValues: PossibleStringValue[] option
}
type Object = {
Collection: bool
Properties: PropertyDocumentationBlock[]
}
type Event = {
Collection: bool
EventProperties: PropertyDocumentationBlock[]
}
type EventRef = {
Collection: bool
EventPropertiesReference: TypeInformation
}
type Function = {
Collection: bool
Parameters: MethodParameterDocumentation[]
Returns: TypeInformation option
}
type TypeInformationKind =
| InfoArray of TypeInformationKind.InfoArray
| InfoString of TypeInformationKind.InfoString
| String of TypeInformationKind.String
| Object of TypeInformationKind.Object
| Event of TypeInformationKind.Event
| EventRef of TypeInformationKind.EventRef
| Function of TypeInformationKind.Function
type TypeInformation =
| WithInnerTypes of kind: TypeInformationKind * innerTypes: TypeInformation[]
| TypeInformation of kind: TypeInformationKind
Where relevant, I would 'embed' inherited fields using a named field.
export declare type MethodParameterDocumentation = {
name: string;
description: string;
required: boolean;
} & TypeInformation;
type MethodParameterDocumentation = {
Name: string
Description: string
Required: bool
// Embedded
TypeInformation: TypeInformation
}
Immediately following the type definition, I would write the Thoth.Json.Net
decoder.
type PropertyDocumentationBlock = {
// Embedded
DocumentationBlock: DocumentationBlock
Required: bool
// Embedded
TypeInformation: TypeInformation
}
module PropertyDocumentationBlock =
let decode: Decoder<PropertyDocumentationBlock> =
Decode.object (fun get ->
{
DocumentationBlock = get.Required.Raw DocumentationBlock.decode
Required = get.Required.Field "required" Decode.bool
TypeInformation = get.Required.Raw TypeInformation.decode
})
In my view, it is better to parse the JSON as close to the source as possible, and then map that within F# to our own representation/domain instead of parsing the JSON directly into our domain.
This should prevent data-loss, and also pick up any changes in the api source generation for electron.
Glutinum FSharp AST is good, but I'm not a fan of having to use fable at this step. Will likely use the FSharp AST as inspiration for the 'glue' ast and then use fantomas for the source generation.
16 October 2025