Ah, so I left out some critical context here. RouteObjects are made into an array (tree) that is then passed to the create*Router functions:
let routes = [
{
RouteObject.id: "root",
path: "/",
caseSensitive: true,
element: <Layout />,
errorElement: <ErrorRoute />,
children: [
{
id: "dashboard",
path: "dashboard",
element: <Dashboard />,
caseSensitive: true,
},
{
id: "settings",
path: "settings",
caseSensitive: true,
element: <Settings />,
errorElement: <ErrorRoute />,
loader: async () => {
let response = await Fetch.fetch("/api/settings", {method: #GET})
if (!Fetch.Response.ok(response)) {
Error.make("Not ok!")->Error.raise
}
switch await Fetch.Response.json(response) {
| data => data
| exception _e => Error.make("Json parsing error")->Error.raise
}
},
},
{
id: "error",
path: "error",
caseSensitive: true,
element: <article>
<h1> {React.string("No Error Page")} </h1>
</article>,
errorElement: <ErrorRoute />,
loader: () => Error.make("rejected")->Error.toException->Promise.reject,
},
],
},
]
This will compile if the values are all left as JSON.t, but then it precludes having mixed response types in the APIs you call. There is also the option of typing all the RouteObject.loader properties to return a Response
I don’t much like either of these options, because I’d like to make the react-router hook useRouteLoaderData be able return a typed response for the route with the specific API response types. I feel I am pretty close to this using the GADT approach I tried after posting last night:
module RouteObject = {
type rec route<'data, 'action, 'handle> = {
id: string,
caseSensitive?: bool,
children?: array<t_raw>,
element?: React.element,
index?: bool,
path?: string,
action?: actionArgs => promise<'action>,
loader?: loaderArgs => promise<'data>,
errorElement?: React.element,
hydrateFallbackElement?: React.element,
handle?: 'handle,
shouldRevalidate?: bool,
@as("lazy") lazy_?: unit => promise<unknown>,
}
and t_raw = Route(route<'data, 'action, 'handle>): t_raw
}
let routes = [
Route({
id: "root",
path: "/",
children: [
Route({
id: "settings",
path: "settings",
loader: ({request: _, params: _}) =>
Fetch.fetch("/api/settings", {method: #GET})
->Promise.then(response => response->Fetch.Response.json)
->Promise.then(json => JSON.Decode.string(json)->Promise.resolve),
}),
Route({
id: "profile",
path: "profile",
loader: ({request: _, params: _}) =>
Fetch.fetch("/api/profile", {method: #GET})
->Promise.then(response => response->Fetch.Response.json)
->Promise.then(json => JSON.Decode.bool(json)->Promise.resolve),
}),
],
}),
]
@module("react-router-dom")
external useRouteLoaderData: (@ignore RouteObject.t<'loaderData, _, _>, string) => 'loaderData = "useRouteLoaderData"
However the issue here is that @unboxed can’t be applied to just t_raw in the mutually recursive type definition, so it ends up being tagged in the compiled form, which wouldn’t work at runtime.
Yeah, I’d say you are right. The issue fundamentally is that it’s not a very rescript-y API I’m trying to bind to. I would rather trade off type safety in the route configuration, which isn’t actually used for much aside from creating the router instance via a binding, in order to get more type safety where consumers would actually use the data, via useRouteLoaderData, if at all possible.