Stuck with array of records having different types

I’m working with the react-table library that provides a formatting function on a column definition. I’m trying to pass in a text column and a date column.

The library wants column definitions like

const cols = [
  {
    accessor: 'name',
    // Value is a string
    Cell: ({ value }) => value,
  },
  {
    accessor: 'date',
    // Value is a JS Date
    Cell: ({ value }) => value.toLocaleString(),
  },
]

I can define a record with a generic cell renderer, but then I have different types that can’t exist in a single array.

type cellValue('v) = {value: 'v};
type genericRenderer('v) = cellValue('v) => string;

let textRenderer: genericRenderer(string) = n => n.value;
let dateRenderer: genericRenderer(Js.Date.t) = d => d.value->Js.Date.toString;

// Compilation Error!
let columns = [|
  {
    "accessor": "name",
    "Cell": textRenderer,
  },
  {
    "accessor": "date",
    "Cell": dateRenderer,
  },
|];

I can define a variant that encapsulates my record types to solve that problem.

type tableColumn =
  | TextColumn(textColumn)
  | DateColumn(dateColumn)

However, I can’t pass in an array tableColumns to react-table. I need to convert from the variant/record to an array of native JS objects that react-table expects.

Is there a solution other than encoding my records to JSON and converting them to plain JS objects?

I guess this is an excellent usecase for @unboxed.
@jsiebern gave a good example in another topic.

Sadly, the best documentation I found so far in the rescript surroundings about @unboxed is an archived blog post.

Edit: You could also use the technique I showed in the first link: define an abstract type and use @bs.obj to create values of this abstract type.

2 Likes

Thanks!

I stumbled upon that post while searching for a solution but didn’t understand it. :slight_smile:

I’ll go educate myself.

Feel free to ask here, if you have some concrete questions.

In general the idea is to finally have a common type you could use for all your objects in your array. Problems arise because in js polymorphic arrays are allowed, but not in rescript (directly).

The @unboxed approach means: use a type of a single variant having a type paramtere for your array, but skip the variant in the runtime representation of your code.
Also known as"unboxed GADT".
See another blog post

The abstract type approach means: you create external functions which let you create different objects which all have the same type. The drawback of this approach is, it’s harder to access values of this abstract type later on - if needed.

For such common use cases, I am thinking of instead of asking people to understand some basic usage of GADT, shall we provide such functions directly:

let Belt.eraseType : 'a => opaque

Note this is still a type safe function from user point of view.

What do you think?

1 Like

I usually create an opaque type and conversion functions for these types of situations:

type renderer;
external toRenderer: 'a => renderer = "%identity";

I agree that GADT’s aren’t really something you want to throw at a newcomer - or in fact at anybody.

@Hongbo What would differentiate Belt.eraseType from identity external?
If opaque would be a predefined type, then I don’t really see a benefit compared to identity external.

I understand we want to move away from objects: Therefore I guess a solution involving records and identity external could be prefered?
I’d suggest a similar approach to the example of @jfrolich (I consider having 'a in an identity external to be dangerous):

Slightly more elaborate example

playground link

module Settings = {
  type settings

  module A = {
    type t = {
      prop1: string,
      prop2: int,
    }
    external toSettings: t => settings = "%identity"
  }

  module B = {
    type t = {
      prop1: string,
      prop2: string,
    }
    external toSettings: t => settings = "%identity"
  }

  module C = {
    type t = {prop3: array<string>}
    external toSettings: t => settings = "%identity"
  }
}

module Example = {
  let sA = {Settings.A.prop1: "42", prop2: 42}->Settings.A.toSettings
  let sB = {Settings.B.prop1: "42", prop2: "42"}->Settings.B.toSettings
  let sC = {Settings.C.prop3: ["42", "43"]}->Settings.C.toSettings
  let allSettings = [sA, sB, sC]
}

I currently still prefer using @bs.obj for creating Js objects to be passed off to a js lib. (especially if they have several optional props)

I would indeed constrain the type more.

1 Like

GADT would be pretty easy to use, no? E.g.

type any = Any('a): any

Usage:

let arr = [
  Any(1),
  Any("two")
]
1 Like