This project showcases how to define a domain model in F# for a Wish List application and then automatically generate TypeScript types using Fable. It also explores how these types can be integrated into a React frontend. The principles shown can be applied to various domains, and the initial focus on EventStorming (see below) is a valuable technique for domain discovery.
EventStorming is a flexible workshop format for collaborative exploration of complex business domains. It brings together domain experts and technical experts to create a shared understanding of a business process. Key characteristics include:
- Collaborative: It involves active participation from diverse stakeholders.
- Visual: It uses sticky notes (often orange for domain events) on a large modeling surface.
- Discovery-focused: It aims to uncover unknown aspects of the domain and identify ambiguities.
- Event-centric: It focuses on "domain events" – significant occurrences within the business process.
Benefits of EventStorming:
- Shared Understanding: Builds a common language and understanding of the domain across different roles.
- Rapid Learning: Allows teams to quickly learn and map out complex domains.
- Identifies Bounded Contexts: Helps in defining clear boundaries between different parts of a system (useful for microservices).
- Highlights Pain Points & Opportunities: Uncovers areas of confusion, bottlenecks, or potential improvements.
- Drives Model Design: The outputs of an EventStorming session (events, commands, aggregates, read models) can directly inform the design of your software, including your F# types for applications like the Wish List example.
-
Conduct the Workshop:
- Gather domain experts, developers, and other relevant stakeholders.
- Use a large modeling space (wall or digital whiteboard).
- Start by brainstorming Domain Events (things that happen in the system, e.g., "Order Placed", "Payment Processed", "Item Shipped"). Write them on orange sticky notes and place them chronologically.
- Identify Commands (user actions or system triggers that cause events, e.g., "Place Order", "Process Payment").
- Identify Aggregates (clusters of domain objects that are treated as a single unit for data changes, e.g., "Order", "Customer"). These often become the entities or records in your F# domain model.
- Define Read Models / Views (data structures tailored for querying or displaying information, e.g., "Order Summary").
- Discuss policies, rules, and external systems.
-
Translate to F# Types:
- The aggregates, events, and supporting value objects identified during EventStorming become the basis for your F# record types, discriminated unions, etc.
- For example, an "Order" aggregate might translate to an
Orderrecord in F# with fields likeOrderId,CustomerId,OrderLines,Status. - Domain events can also be modeled as F# types, often as part of a discriminated union representing all possible events.
This project is set up to automatically generate TypeScript types from your F# domain models defined in Domain.fs using Fable.
- .NET SDK (Version 9.0 or higher, this project specifically uses .NET 9)
- Node.js and npm (LTS version recommended)
- Clone the repository (if you haven't already).
- Restore .NET tools:
dotnet tool restore
- Install Node.js dependencies:
npm install
-
Open the
Domain.fsfile. Define your F# types (records, discriminated unions, etc.) within theModelsnamespace. For the Wish List application, this includes types likeWishList,WishListItem, andUserProfile. For example:namespace Models // Example: WishListItem from Domain.fs type WishListItemDetails = { Title: string Description: string option Url: string option } type WishListItem = { ItemId: System.Guid Details: WishListItemDetails AddedDate: System.DateTimeOffset IsPurchased: bool Priority: int } // Other types like UserProfile, WishList, WishListVisibility, Address are also defined in Domain.fs
-
Once you have defined or updated your F# types in
Domain.fs, run the following command in your terminal:npm run build:ts
-
This command uses Fable to transpile your F# types into TypeScript.
-
The generated TypeScript files will be placed in the
dist/tsdirectory.
The TypeScript files generated in the dist/ts directory can be directly imported into your TypeScript projects (e.g., a React front-end, a Node.js backend service). The main file containing your translated types will be dist/ts/Domain.ts (derived from Domain.fs).
Example:
Assuming your project is set up to resolve TypeScript modules, you can import types like so. (The example types here are from the Wish List domain):
// In your frontend/typescript project
// Adjust path as needed. Some bundlers/setups might allow omitting '.js'
import { UserProfile, WishList, WishListItem, WishListVisibility } from "./path/to/dist/ts/Domain";
// Or, to be more explicit, if your moduleResolution supports it:
// import { UserProfile, WishList } from "./path/to/dist/ts/Domain.js";
// Example function using WishList types
function displayWishListInfo(wishList: WishList) {
console.log(`Wish List: ${wishList.ListName}`);
console.log(`Owned by: ${wishList.Owner.FirstName}`);
console.log(`Number of items: ${wishList.Items.length}`);
// Working with a discriminated union like WishListVisibility
switch (wishList.Visibility.Case) {
case "Private":
console.log("This list is private.");
break;
case "SharedWithLink":
console.log("This list is shared via a link.");
break;
case "SharedWithUsers":
console.log(`Shared with ${wishList.Visibility.Fields[0].length} users.`);
break;
case "Public":
console.log("This list is public.");
break;
}
}Important Considerations:
fable_modules: The generated code imports helpers from thedist/ts/fable_modulesdirectory (which contains parts offable-library-ts). When you use these types in another project, you'll need to ensure that these dependencies are resolvable.- Typically, you would add
fable-library(orfable-library-ts) as a dependency to your consuming TypeScript project. - Alternatively, if you copy the
dist/tsfolder, ensure you copyfable_modulesalong with your domain types.
- Typically, you would add
- Module Resolution: How you import (
DomainvsDomain.jsvsDomain.ts) can depend on yourtsconfig.jsonsettings (specificallymoduleResolutionandallowImportingTsExtensions) in the consuming project and your bundler's capabilities. Fable's default output fromfableconfig.jsonwith"lang": "ts"should produce.tsfiles. - Type Complexity: Refer to the Fable documentation for specifics on how F# types (options, DUs, lists, Guid, decimal etc.) are translated to TypeScript. The generated
_$reflectionfunctions are for Fable's internal use but show the structure.
- Explore the Wish List application types in
Domain.fs. - Modify or extend the Wish List domain with new features (e.g., item categories, ratings).
- Run the
npm run build:tscommand to see the generated TypeScript for your changes. - Integrate the generated types into a new or existing TypeScript project (Node.js, Deno, or a web frontend).
For a conceptual guide on how these F#-generated TypeScript types can be used within a React application (potentially with Material UI and Module Federation), please see:
Conceptual React Integration Guide
This project includes a runnable TypeScript example to demonstrate how to import and use the F#-generated types.
The example is located in the examples/ts-usage-example directory.
Ensure you have already:
- Built the F# types to generate the TypeScript output in the
dist/tsdirectory:(Run this from the project root directory)npm run build:ts
-
Navigate to the example directory:
cd examples/ts-usage-example -
Install example-specific dependencies: This will install
ts-node,typescript, andfable-library-tslocally for the example.npm install
-
Run the example script:
npm start
This command executes
ts-node run_example.ts. You should see output in your console demonstrating the creation and manipulation of the typed objects.
The run_example.ts script shows how to:
- Import types from
../../dist/ts/Domain. - Import helpers like
somefromfable-library-ts/Option. - Instantiate records and discriminated unions.
- Work with optional properties and discriminated union cases.
Refer to the examples/ts-usage-example/run_example.ts file for the complete code.
You can also run npm run build_types from within the examples/ts-usage-example directory as a shortcut to rebuild the F# types from the root.
For users interested in Deno, a separate example is available that demonstrates using the F#-generated types in a Deno environment.
This example showcases Deno's native TypeScript capabilities and its module import system.
- Location:
examples/deno-example/ - Instructions: See the detailed
README.mdwithin theexamples/deno-example/directory for setup and execution steps.