How to enforce that two function signatures align (Next.js serverSideProps and Page)

Hello!
I need help to write a functor that enforces the relation between a Next.js page and it’s corresponding getServerSide props function.
First I tried creating a single functor that takes a type, then forces all the passed down functions (make page and also getServerSideProps) to return/accept compatible types. That was my desired solution because it was strongly typed, I can annotate the output of the functor, so only what is needed is exposed in the exports, and everything was nice and awesome until I faced the reality.
For reference, here is the code of the functor that compiles, is correct, but does not work in the real world:

module type PageSignature = {
  [@decco]
  type t;
  let name: string;
  let getServerSideProps:
    Next.GetServerSideProps.context => PromiseResult.t(t, string);
  let make: t => React.element;
};

module type GeneratedPage = {
  let getServerSideProps:
    Next.GetServerSideProps.context => Promise.t(Js.Json.t);
  let default: (~props: Js.Json.t) => React.element;
};

module Make = (S: PageSignature) : GeneratedPage => {
  [@decco]
  type serverProps = {props: S.t};
// ... bla bla bla
}

If I have this already, why am I bothering you? well, the problem, is that I must do the serverSideProps and the default export assignments separated, because NextJs is not smart enough to remove code that is only used in getServerSideProps.
Any reference of any BE library to any intermediary variable will produce an import error.
To give you an example, here is what using that functor looks like:

include Dev__Page.Make({
  [@decco]
  type t = string;
  let name = "Stories";
  let make = title =>
    <div>
      <h1> title->React.string </h1>
      <main> "Wow, worked"->React.string </main>
    </div>;

  let getServerSideProps = _ => referenceToBeFunction()
});

And the produced code (simplified):

// imports that use BE deps that are then used inside getServerSideProps

var include = Dev__Page$.Make({
      t_encode: t_encode,
      t_decode: t_decode,
      name: "Stories",
      getServerSideProps: getServerSideProps,
      make: make
    });

var getServerSideProps$1 = include.getServerSideProps;

var $$default = include.$$default;

export {
  getServerSideProps$1 as getServerSideProps,
  $$default ,
  $$default as default,
}

Do you see those intermediary assignments? like getServerSideProps: getServerSideProps and var getServerSideProps$1 = include.getServerSideProps;. That is enough to break Next.js code split algorithm

(If you want a fantastic explanation of the problem this stack-overflow answer is awesome)

Then I thought that maybe I can make two functors, one depending on the output of the other and use the result of the second functor to generate the server side props, but I can’t imagine how to make this work.

I also tried returning a function in the functor that gets another function and wraps it to ensure the types match, but then the outside is not able to see the type of GeneratedPage.t and therefore it fails to type check. Modified version fo the functor to do this:

type getProps('t) =
  Next.GetServerSideProps.context => PromiseResult.t('t, string);

module type PageSignature = {
  [@decco]
  type t;
  let name: string;
  let make: t => React.element;
};

module type GeneratedPage = {
  type t;
  let makeGetServerSideProps:
    (getProps(t), Next.GetServerSideProps.context) => Promise.t(Js.Json.t);
  let default: (~props: Js.Json.t) => React.element;
};

module Make = (S: PageSignature) : GeneratedPage => {
/* same bla bla as before */
}

Then use it like this:

module P =
  Dev__Page.Make({
    [@decco]
    type t = theReturnedType;
    let name = "Stories";
    let make = //bla bla bla react
  });

let default = P.default;
let getServerSideProps =
  P.makeGetServerSideProps( something => theReturnedType );

But, as I said, makeGetServerSideProps only accepts GeneratedPage.t, so it is not valid.

Why do I need a functor in the first place? Well, there are some particularities around Next.js injecting props into the component, serialisation and error handling that I want to abstract, and I think a functor is the right tool for this and create a nice JS output.

This is the best thing I was able to get, two functors that you have to make sure you provide the same base type to both:

module type PageSignature = {
  [@decco.decode]
  type t;
  let name: string;
  let make: t => React.element;
};

module type GeneratedPage = {let default: (~props: Js.Json.t) => React.element;
};

// Pass the minimum because, why should the Dev page worry about anything else?
type getProps('t) =
  (~query: Js.Dict.t(string)) => PromiseResult.t('t, string);

/**
  Sadly, Server and Page must be two separate functors.
  The reason is that Next.js is not smart enough to figure out
  what parts of the generated code to remove when sending the page to the FE.
  All it knows is that, everything inside getServerSideProps must be removed, and that's all.
  If you make any assignment to any variable that is not named getServerSideProps that dependency
  will not be removed.
*/
module type ServerSignature = {
  [@decco.encode]
  type t;
  let name: string;
  let getProps: getProps(t);
};

type context = Next.GetServerSideProps.context;

module type GeneratedServer = {
  let getServerSideProps: context => Promise.t(Js.Json.t);
};

module Make = (S: PageSignature) : GeneratedPage => {
  [@decco.decode]
  type serverProps = {props: S.t};
/* bla bla bla*/

}
module MakeServer = (M: ServerSignature) : GeneratedServer => {
  [@decco.encode]
  type serverProps = {props: M.t};
/* bla bla bla*/
}

Your overall problem and solution remain a little unclear to me (maybe a full working example will help with that, but I understand that it may not be possible, or it may not be worth it to post).

But as to these issues:

I see you have one solution in your second post. If that is not satisfactory to you, you could make the GeneratedPage.t non-abstract in the module type. Or if you want it abstract in the module type, you can use Sharing Constraints to do it (there’s a section in that chapter on it…the link is not direct though, sorry.). They will let you tell the compiler what types your actualized implementation have for that signature.

Here is a playground with a silly example just to show the constraint syntax in rescript, rather than OCaml.

By the way, I see you also posted this question on the OCaml discuss forum. It would probably be a good idea to link to both of these.