I'm working on some bindings for a Remix app and I could use some help with a functor to generate a loader module

I finally found my first use case for a functor.

Here’s my goal. Remix has a loader function and a useLoaderData function. With TypeScript you add in a typeof loader to the useLoaderData and the response from that hook is typed correctly.

import { useLoaderData, json } from "@remix-run/react";

import { prisma } from "../db";

export async function loader() {
  return json(await prisma.user.findMany());
}

export default function Users() {
  const data = useLoaderData<typeof loader>();
  return (
    <ul>
      {data.map((user) => (
        <li key={user.id}>{user.name}</li>
      ))}
    </ul>
  );
}

It’s almost exactly the same as the return type of json except it strips out the promises from the types, so instead of promise<user> we just get user.

Here’s what I have so far in ReScript.

// Remix.res
module Loader = {
  type json<'a> = 'a
  @module("@remix-run/react")
  external json: {..} => json<'a> = "json"

  type t<'a> = unit => promise<'a>
}

module type LoaderData = {
  type t
}

module MakeLoader = (Data: LoaderData) => {
  type json = Loader.json<Data.t>
  type t = Loader.t<Data.t>

  @module("@remix-run/react")
  external useLoaderData: unit => Data.t = "useLoaderData"
}

// _index.res
open Webapi

let headers: Remix.Headers.t = (
  ~_actionHeaders,
  ~_errorHeaders,
  ~_loaderHeaders,
  ~_parentHeaders,
) =>
  {
    "Cache-Control": "max-age=300, s-maxage=3600",
  }

module Data = {
  type t = {"foo": string, "data": array<string>}
}

module Loader = Remix.MakeLoader(Data)

let loader: Loader.t = async () => {
  Remix.Loader.json({
    "foo": "bar",
    "data": await Fetch.fetch("https://baconipsum.com/api/?type=meat-and-filler")->Promise.then(
      Fetch.Response.json,
    ),
  })
}

@react.component
let make = () => {
  let data = Loader.useLoaderData()  // type here is correct: {"foo": string, "data": array<string>}
  <>
    <Home_hero />
  </>
}

This is pretty good, but I would really love it if I could have type checking work across what is being passed to json and the type returned from useLoaderData.

In this example I would need {"foo": string, "data": array<string>} to somehow match up to {"foo": string, "data": promise<array<string>>}.

I’m hoping someone has a clever idea.

Ah, I figured it out!

I moved the binding to json into MakeLoader and then change up how I pass the data to json.

// Remix.res
module Loader = {
  type t<'a> = unit => promise<'a>
}

module type LoaderData = {
  type t
}

module MakeLoader = (Data: LoaderData) => {
  type t = Loader.t<Data.t>

  @module("@remix-run/react") external json: Data.t => 'b = "json"

  @module("@remix-run/react")
  external useLoaderData: unit => Data.t = "useLoaderData"
}

// _index.res
module Data = {
  type t = {"foo": string, "data": array<string>}
}

module Loader = Remix.MakeLoader(Data)

let loader: Loader.t = async () => {
  let data = async () =>
    {
      "foo": "bar",
      "data": await Promise.resolve(["one"]),
    }

  Loader.json(await data()) // type error if this doesn't match Data.t
}

@react.component
let make = () => {
  let data = Loader.useLoaderData() // type here is Data.t
  <>
    <Home_hero />
  </>
}
2 Likes