Type as a value + undocumented (?) syntax

I’ve been looking into a way to make a general Request.make(...) function, where I could specify the return type when calling it. And stumbled upon the rescript-request library which can do that:

Request.make(
  ~url="/api/health",
  ~responseType=(JsonAsAny: Request.responseType<response>),
  ()
)

The problem is, thought it works if I copy paste the parts I need in project, I can’t figure out how. I’ve been looking at the source code and see unfamiliar syntax that I was not able to find documentation for. These are the confusing bits:

responseType=**(JsonAsAny:** Request.responseType<response>**)**, ...
type rec responseType<'response> =
  **| JsonAsAny:** responseType<'response>
let make = (
  **type payload body**, ...

I’d appreciate any documentation links or explanations on the syntax.
And iIf I’m only using JsonAsAny can I somehow not have (what I’m assuming are) variants in the responseType?
Also I’m using module functors to abstract various APIs, by providing variants for possible actions and api function to get the params for each action, but response types don’t seem to work with this approach. I’ve added an example here.

1 Like
type rec responseType<'response> =
  | JsonAsAny: responseType<'response>

This is called GADT (Generalized Algebraic Data Type). This isn’t mentioned anywhere in the documentation most probably because it is an advanced topic.

If you’re familiar with Haskell/Ocaml then it works very similar to GADT in those languages.

GADT allows you to specify the polymorphic variable for each of your constructor. First let’s look at the normal variants or sum types.

type responseType =
  | JsonBool(bool)
  | JsonString(string)
  | JsonAny

we can then observe the type signature for each of the constructor.

JsonBool : bool => responseType
JsonString : string => responseType
JsonAny : responseType

They all return the same responseType. What if you want to differentiate the return type?

Let’s do that.

type responseType<'response> =
  | JsonBool(bool)
  | JsonString(string)
  | JsonAny

We added a polymorphic variable 'response to capture the return type but it’s not being used anywhere in our constructors. (btw, this is called phantom type).

Then we can do,

let x: responseType<string> = JsonString("hello")
let y: responseType<bool> = JsonBool(true)

Both x and y now have a different type, but you’d have to explicitly annotate the type. Also this isn’t entirely safe.

let x: responseType<bool> = JsonString("hello") // Oh no!

What we can do is use GADT to differentiate the return type.

type rec responseType<'response> =
  | JsonBool(bool) : responseType<bool>
  | JsonString(string) : responseType<string>
  | JsonAny : responseType<'response>
let x = JsonString("hello") 
let y = JsonBool(true)

x has type responseType<string> and y has type responseType<bool>. If you do something like,

let x: responseType<bool> = JsonString("hello") // we get compile error now. Nice!

This is the reason why the code in your playground doesn’t work. You had different types,

 let api = (action: action) => {
   switch action {
   | GetExample => {
       url: "http://example.com",
       typ: (ResType: Request.responseType<resForExample>),
     }
   | GetGoogle => {
       url: "http://example.com",
       typ: (ResType: Request.responseType<resForGoogle>),
     }
   }
 } 

As for JsonAny constructor, you’d need to specify the return type because there’s still polymorphic variable 'response.

If you want to know the use case of this, I recently wrote a blog post that uses this method here Approaches to Type Safe JS Interop Design

I hope that helps.

10 Likes

There’s an issue regarding this, BTW

“This one is ridiculously hard to describe in an intuitive way”

@shulhi 's description is quite good, might serve as a base for the docs

3 Likes

To add to what has been already said, the let make = (type payload body,... syntax is for what’s called “locally abstract types.” They’re also a very advanced feature, and almost always needed for dealing with GADTs. AFAIK, they aren’t documented in ReScript either yet, but they work just like they do in OCaml. (If you want to find resources about them.)

4 Likes

@shulhi thank you for the detailed explanation!
Looking at the Proxy example in your article was particularly helpful to wrap my head around GADT syntax.