I often build some functions which will traverse my types at runtime and print them to stdout.
So I can run these scripts with node / bun and pipe the output to another file.
Here is a very simple example:
// Type Gen
let makeBuilder = (form) => {
Console.log("type t = {")
form->Obj.magic->Dict.forEachWithKey((field, fieldName) => {
let dt = switch field {
| JSON.String(_) => "option<string>"
| JSON.Number(_) => "option<int>"
// ...
}
Console.log(` ${fieldName}: ${dt},`)
})
Console.log("}")
}
type form = {
name: string,
age: int,
}
makeBuilder({
name: "",
age: 0,
})
// Result
type t = {
name: option<string>,
age: option<int>,
}
If the runtime type is not enough, you could also add some helper functions for the fields:
// Type Gen
let makeBuilder2 = (form) => {
Console.log("type t = {")
form->Obj.magic->Dict.forEachWithKey((field, fieldName) => {
Console.log(` ${fieldName}: ${field},`)
})
Console.log("}")
}
let string: string = Obj.magic("option<string>")
let int: int = Obj.magic("option<int>")
let float: float = Obj.magic("option<float>")
type form2 = {
someString: string,
someInt: int,
someFloat: float,
}
makeBuilder2({
someString: string,
someInt: int,
someFloat: float,
})
// Result
type t = {
someString: option<string>,
someInt: option<int>,
someFloat: option<float>,
}
Of course, you have to handle nested types, variants, etc, but just the idea.
I know, It’s a bit more boilerplate, but
- it’s simple rescript code
- it doesn’t need a complex toolchain, parsers, whatever
If you would run a small script (maybe a filewatcher) on a predefined folder, you could also split these types into different input and output files.
You can find a more complex example of my approach in my sql query builder.