Skip to content

X-FRI/Medplum.FSharp

Repository files navigation

中文 | English


Medplum.FSharp

F# type definitions for FHIR R4, converted from the TypeScript definition package @medplum/fhirtypes.

Conversion Method

This library was generated by converting TypeScript declaration files to F# using ts2fable. The conversion pipeline consists of four distinct stages.

First, the TypeScript .d.ts files are extracted from the npm package and converted to F# stubs using ts2fable. Each of the 211 source files produces a corresponding F# module with type definitions that preserve the original structure. The ts2fable tool translates TypeScript interfaces to F# abstract types with [<AllowNullLiteral>] attributes, converts string literal unions to discriminated unions with [<RequireQualifiedAccess>], and handles generic type parameters with appropriate constraints.

Second, the generated F# code undergoes extensive post-processing to remove Fable-specific dependencies. The original ts2fable output references Fable.Core and its JavaScript interoperability layer, which are unnecessary for a pure .NET library. The post-processing script removes open Fable.Core and open Fable.Core.JS statements, replaces ResizeArray<'T> with standard F# arrays 'T[], and eliminates Fable-specific attributes including [<Erase>], [<Pojo>], and [<StringEnum>].

Third, the conversion pipeline addresses type system incompatibilities between TypeScript and F#. TypeScript allows forward references and structural typing, while F# requires nominal types with explicit declaration order. The pipeline defines Fable-compatible union types (U2 through U10) in a dedicated module, fixes type aliases that reference undefined symbols, and resolves forward reference violations by reordering type definitions or substituting obj where mutually recursive types would otherwise be required.

Fourth, the files are organized into a coherent namespace hierarchy. The 162 resource types are placed in the Resources/ directory, 35 reusable complex types in ComplexTypes/, 7 primitive data types in PrimitiveTypes/, and core infrastructure types remain in Types/. A root Medplum.fs module re-exports commonly used types to provide a convenient public API.

The complete conversion process can be summarized as:

flowchart LR
    A["@medplum/fhirtypes npm package"] --> B["ts2fable conversion<br/>211 .d.ts → 211 .fs files"]
    B --> C["Post-processing<br/>Remove Fable dependencies<br/>Fix type system issues"]
    C --> D["Organization<br/>Resources/, ComplexTypes/,</>PrimitiveTypes/, Types/"]
    D --> E["Final library<br/>0 build errors<br/>14 passing tests"]
Loading

Current Limitations

The nature of this conversion introduces several limitations that users must understand.

Most significantly, all FHIR types are represented as abstract interfaces rather than concrete record types. This design stems from the ts2fable translation of TypeScript interfaces to F# [<AllowNullLiteral>] abstract types. Users cannot construct instances using F# record syntax. Instead, instances must be created through deserialization from JSON or by implementing the interface, which significantly impacts usability for F#-centric workflows.

The type signatures preserve TypeScript patterns rather than adopting idiomatic F# conventions. For example, optional properties use option<'T> rather than 'T voption, arrays are represented as 'T[] rather than list<'T>, and union types use explicit Fable-style U2<'T1, 'T2> discriminated unions. These choices maintain fidelity to the original TypeScript definitions but deviate from what F# developers would expect in a hand-written library.

Generic type parameters have been simplified in many cases. The original TypeScript definitions include complex generic constraints such as Reference<T extends Resource>, but the conversion pipeline often simplifies these to non-generic Reference types to avoid forward reference issues. This loss of generic specificity reduces type safety in scenarios where the referenced resource type matters.

Several TypeScript language features have no direct F# equivalent. Type-level utilities like ExtractResource<K> and conditional types cannot be represented in the F# type system and have been removed or replaced with simpler approximations. String literal types that would ideally compile to constants are represented as discriminated unions, which introduces runtime overhead for pattern matching.

The generated code includes workarounds for TypeScript-specific patterns that may not integrate cleanly with standard .NET libraries. The [<AllowNullLiteral>] attribute enables null values for interface types, which conflicts with F#'s preference for option types to represent nullability. Users interoping with C# code or other .NET languages must be aware that these interfaces accept null values despite F#'s general avoidance of null.

Usage

Install the library via NuGet or add a project reference directly.

dotnet add package Medplum.FSharp

The primary use case involves deserializing FHIR JSON into typed objects. System.Text.Json can deserialize JSON into the abstract interface types because they expose property setters.

open System.Text.Json
open Medplum.Resources
open Medplum.ComplexTypes

let json = """{
  "resourceType": "Patient",
  "name": [{"family": "Smith", "given": ["John"]}],
  "gender": "male"
}"""

let patient = JsonSerializer.Deserialize<Patient>(json)
// patient.Value.Name contains array of HumanName
// patient.Value.Gender is Some PatientGender.Male

Pattern matching on option types requires handling both Some and None cases.

match patient.Value.Gender with
| Some PatientGender.Male -> printfn "Male patient"
| Some PatientGender.Female -> printfn "Female patient"
| Some _ -> printfn "Other gender"
| None -> printfn "Gender not specified"

The root Medplum namespace provides access to all FHIR types without requiring nested open statements.

open Medplum

// Resource types are available directly
let p: Patient.Resource = Unchecked.defaultof<Patient.Resource>

// Complex types are similarly accessible
let n: HumanName = Unchecked.defaultof<HumanName>

Remaining Work

The current implementation represents a functional baseline rather than a production-ready library. Several areas require further development.

The abstract interface types should be replaced with concrete record types or class types that support direct construction in F#. This change would require a significant departure from the conversion approach, as it would demand hand-written type definitions or a sophisticated post-processing pipeline capable of synthesizing constructors.

Generic type constraints need proper implementation. The current simplification of Reference<T extends Resource> to non-generic Reference loses type information that prevents the compiler from verifying referential integrity. Restoring these generics would require resolving the forward reference issues that led to their removal, likely through strategic use of the and keyword for mutually recursive type definitions.

The module organization could benefit from a more granular namespace structure. The current flat module hierarchy places all 162 resources in a single directory, which makes navigation difficult. Organizing resources by clinical domain (e.g., Medplum.Resources.Clinical, Medplum.Financial, Medplum.Medications) would improve discoverability but would require updates to the conversion scripts.

Additional tooling would enhance the developer experience. A source generator that creates type-safe JSON serialization attributes, a code generator that produces F# record wrappers for the interface definitions, or an F# script that scaffolds resource instances with required fields populated would all reduce the friction of working with the current interface-based design.

Comprehensive test coverage is currently lacking. The existing 14 unit tests verify basic functionality but do not exercise the full breadth of the 211 type definitions. Property-based tests using FsCheck could verify that serialization round-trips preserve data integrity, and integration tests could validate compatibility with real FHIR JSON from production systems.

Documentation for each FHIR resource type would improve usability. The current XML doc comments are preserved from the TypeScript source but are not visible to IntelliSense without an additional documentation generation step. Publishing the API documentation to a hosting service would make the library more approachable for new users.

Performance characterization is needed to understand the overhead of the abstract interface design compared to hand-written record types. Benchmarking serialization and deserialization operations would quantify whether the conversion approach imposes meaningful performance penalties.

Finally, establishing a synchronization mechanism with the upstream @medplum/fhirtypes package would ensure that FHIR updates propagate to the F# library in a timely manner. The current scripts/update.sh script automates re-running the conversion pipeline, but each update may require manual adjustments to address new type system incompatibilities or TypeScript language features.

About

F# type definitions for FHIR R4, converted from the @medplum/fhirtypes TypeScript package.

Resources

License

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages