Intersection Types


#1

Does anyone know of a way to achieve something similar to TypeScript intersection types in Reason? Here’s the context.

I have a [bs.deriving abstract] with a set of known fields.

[@bs.deriving abstract]
type renderArgs('data) = {
    fetching: bool,
    loaded: bool,
    data: 'data,
    error: string
};

The user then supplies a map of mutations they want to make available, which I have modeled currently as a Js.Dict.t (open to changing that). Is there any way I can intersect this Js.Dict.t with renderArgs to create an “intersection type” in the TypeScript sense. In TS, the definition looks like this:

declare type RendersArgs<Data, Mutations> = {
    fetching: boolean;
    loaded: boolean,
    data: Data,
    error: string
} & Mutations;

Is something like this possible with Reason / BuckleScript? Thanks for any help!


#2

Hi, @parkerziegler!

If I’m not mistaken, what you’re looking for uses what’s called “structural typing” instead of the normal “nominal typing”. Where a type is identified by its attributes and properties rather than just its name. The only structural typing I’m aware of in ReasonML is around the Js.t type.

According to my experience, Js.Obj.assign will take a Js.t('a) and a Js.t('b) and produce a Js.t('c) which is the union of the fields on 'a and 'b.

Am I mistaken about this, @bobzhang?


#3

Thanks @splodingsocks! It looks like Js.Obj.assign works well with values but doesn’t provide a type constructor: https://reasonml.github.io/en/try.html?rrjsx=true&reason=C4TwDgpgBAhlC8UDeAoKUB0aoCJICcBnAewDscAuKQ4fAS1IHMUBfAbhRVEigCMFk2LOhwBjYgBti+StVoNm7Tt2iiBAKUIYA8rwBWGGIUJ1GpABQwANHwCUbIA.

The issue I’m running into is I need to type the argument passed to my render prop, which, at runtime, is an object created from known fields (my @bs.deriving abstract above) and a user supplied map of mutation functions (my Js.Dict.t above, modeled as an object on the JS side).

I did find a way to get around this, which involved just passing a type parameter 'mutations to renderArgs. This means that access to individual mutations happens with one additional level of access (i.e.) result |. mutations |. <mutation> vs. result |. <mutation>. So a small change to the API, but one that I actually think makes it better in some ways.

Basically, would love to know if there’s a Reason equivalent to TS &.


#4

I think your solution is a good one. Reason is more composition-oriented than TS, it makes sense to compose the mutations under a field in the renderArgs type. There is a mechanism for intersection but it’s a rather advanced technique. You’d have to deal with polymorphic variant types as a phantom type parameter and deal with their proper subtyping rules.


#5

generally speaking, that mixing of the dict with the object is unsafe—it’s not defined what behavior you want if any of the mutations have a key named “fetching”, “loaded”, “data”, or “error”.

if you can get Mutations to a defined state or interface, there are multiple mechanisms in ocaml to achieve structural, intersection typing: modules and object types

your way to get around this, though, with basic composition, is much simpler than either of those approaches, and better—so congrats!


#6

Hey all,

So I’m actually running into an issue here. I thought I had it working but apparently I didn’t.

The issue I’m running into is this. The lib I’m binding to, Urql (https://github.com/FormidableLabs/urql), does a trick of turning each mutation in the user provided mutation map into a field on the object passed to the render prop. To give an example:

The user provides a mutation prop like so (Reason example):

let mutationMap: Connect.mutationMap = Js.Dict.empty();
Js.Dict.set(mutationMap, "likeDog", likeDog);

<Connect
   mutation={mutationMap}
/>

The library then binds a mutate method to each mutation behind the scenes, and spreads those mutations onto the object passed to the render prop. So, to summarize, the render prop gets an argument that looks like:

[@bs.deriving abstract]
type renderArgs('data) = {
  fetching: bool,
  loaded: bool,
  data: 'data,
  error,
  refetch,
  refreshAllFromCache,
  likeDog: <user provided type>
};

Is there a way to add fields to a [@bs.deriving abstract] on the user’s end? Basically, all properties other than user supplied mutations are fixed (fetching, loaded, data, etc.). The object then gets the additional likeDog added to it at runtime. Is there any way to type that? My thought to add an extra key called mutations leads to compiled JS access that is: result.mutations.likeDog which evaluates as undefined – I need result.likeDog.

To be clear, the user knows the shape ahead of time, I just think the bindings should do the job of providing all the fixed fields and the user should provide the types for the mutations they tack on. Thoughts?


#7

Yeah, I don’t think there’s a way in Reason to “smoosh” two object types together and retain safety :confused:


#8

what @jaredly said: you’re not going to be able to do this directly in a safe way. If you are just using the library directly, I’d say try writing a small function which takes this from a Js.t({..}) or whatever to a record with all the framework-defined fields PLUS a separate mutations map.

If you are trying to build a reusable binding to this library, you’d probably want to abstract that Js.t({..}) messiness behind a module and an abstract .t type.

Sorry it’s such a bummer. And the creator of the urql library can never add more framework fields to that structure without risking conflicts with/breakage of client code, either.


#9

And the creator of the urql library can never add more framework fields to that structure without risking conflicts with/breakage of client code, either.

Another great selling point for keeping your types abstract and offering functions to construct them instead.