I’ve been working on a new library that automatically generates fluent builder patterns for ReScript types, and I’d love to get some feedback from the community before continuing development.
What it does
rescript-derive-builder scans your ReScript codebase and generates type-safe builder patterns for types annotated with @@deriving(builder). Here’s a quick example:
/**
* @@deriving(builder)
* User profile with optional fields
*/
type t = {
name: string,
age: int,
email?: string, // Optional field support!
}
Generates:
let user = UserBuilder.empty()
->UserBuilder.name("Alice")
->UserBuilder.age(25)
->UserBuilder.email(Some("alice@example.com"))
->UserBuilder.build()
// Result: result<User.t, string> with specific error messages
GitHub: GitHub - nathan-tranquilla/rescript-derive-builder
NPM: https://www.npmjs.com/package/rescript-derive-builder
Design Decisions I’d Love Feedback On
1. Using rescript-tools doc for Type Extraction
Instead of using PPX, I chose to use rescript-tools doc to extract type information:
Pros:
-
Leverages the official ReScript toolchain -
Gets accurate type information (handles all ReScript syntax) -
JSON output is stable and well-structured -
Future-proof against ReScript syntax changes -
Allows me to write codegen in ReScript!
Cons:
-
Requires an extra compilation step
Question: Does this approach feel right to the community? Are there concerns about the approach?
2. Docstring Annotations + JSON Config
I went with a hybrid approach:
-
Docstring annotations (
@@deriving(builder)) mark which types to process - JSON configuration within the rescript.json file specify file patterns and output directories
{
"derive-builder": {
"include": ["src/**/*.res"],
"output": "src/generated"
}
}
Rationale:
- Docstring annotations keep the intent close to the type definition
- JSON config handles project-level concerns (file patterns, output paths)
Question: How does this feel compared to any alternative approaches like a dedicated config file for the builder?
3. Strategy Pattern Architecture
The library uses a chain-of-responsibility pattern for code generation:
module type HandlerInterface = {
let canHandle: JSON.t => bool
let handle: JSON.t => string
}
This makes it easy to add support for different type patterns by implementing new handlers.
Current Limitations (Intentional)
Right now, the library only supports the main type pattern (single type t within a file):
// ✅ Supported
type t = { name: string, age: int }
// ❌ Multiple non-t types not yet supported
type user = { name: string, age: int }
...
Why start here?
- Most common ReScript pattern (module-scoped
.ttypes) - Validates the architecture before expanding
- Keeps initial scope manageable
Key Features
-
Optional field support with ReScript’s
field?: typesyntax - Comprehensive error messages for missing required fields
- Type-safe builders with proper ReScript typing
- Automatic discovery of configuration files (looks for the first rescript.json file in parent having the “derive-builder” configuration key)
- Extensible architecture for future type patterns
Questions for the Community
-
Architecture: Does the
rescript-tools doc+ docstring approach feel idiomatic? - API Design: How does the generated builder API feel? Any ergonomic issues?
- Limitations: What type patterns should I prioritize next?
- Performance: Any concerns about the build-time code generation step?
- Integration: How would this fit into your existing ReScript workflows?
Next Steps
Next steps would involve expanding type generation for other type patterns
Try it out:
npm install rescript-derive-builder
npx rescript-derive-builder
I’d really appreciate any thoughts, concerns, or suggestions! This is my first major ReScript library, so community input is invaluable for making sure I’m heading in the right direction.