How To Type Unknown Data Structures


#1

I’m working on bindings for an external JS lib (written in React), and I’m running into some issues with understanding how to type data with unknown shapes. For example, the library exposes a render prop pattern, where a user gets access to data as the argument to the render prop. The shape of data could be anything – an, array, list, Js.t, etc. So it looks kind of like this:

type renderProp = {.
   "data": <how do I type this???>
}

let make = (~renderProp: renderProp => ReasonReact.reactElement , _children) =>
  ReasonReact.wrapJsForReason(
    ~reactClass=fakeComponent,
    ~props={
      "renderProp": renderProp
    },
    _children
  );

In general, I don’t understand in Reason how to say, “I don’t know what the shape of data is, but I need to have it available.” Whenever I go to use this component, I get errors trying to access properties or do operations on data b/c I don’t have a defined type for it. Any pointers on typing data with unknown shape?


#2

If it can really be anything you can use something like this:

type renderProp = {.
   "data": 'anything
};

That leaves you open to runtime errors though and you’re losing all the good type safety stuff.

Can you make a guess as to what the data will be before use? Could you use a functor maybe to keep the component dynamic and let the user of the component pass the expected type?

Edit:
Actually sorry, you’d need to pass the 'anything to the type as an argument that way.

type renderProp('anything) = {.
   "data": 'anything
};

Which probably isn’t what you want.

You could do this then:

let make = (~renderProp: 'anything => ReasonReact.reactElement, _children) =>
    ReasonReact.wrapJsForReason(
      ~reactClass=fakeComponent,
      ~props={"renderProp": renderProp},
      _children,
    );

#3

If data structures are completely unknown, then you are in troubles.

If your data structures are known, then there is a way., If the same parameter to function can take absolutely different shapes, then you can actual make it type proof.

I had similar situation: in production we have to convert ‘old’ API calls into ‘new’ API calls. Old input had absolutely different input structures.

At first, you have to define all possible type of data structures. Then write transform functions, which based on input, will emit exact data type you have.

For example:

/* first data structure */
type metricCount = {type_: string};

/* another data structure, but it may appear in the same input parameter */
type metricWithLabel = {
  type_: string,
  label: string
};

let createCountWithLabel = (label: string) : metricWithLabel => {type_: "COUNT", label};

let countMetric: metricCount = {type_: "COUNT"};

/* oldJson might be your unknown data structure */
let transformToCount20 = (oldJson) => {
  /* here I used Json.Decode and, by checking oldJson's label propery I know 100% that
oldJson is of type metricWithLabel. So all next calls to createCountWIthLabe, MetricEncoder...
are type proofed during compilation time. Json.Decode actually also provides verification at runtime
 */
  let label = Json.Decode.(oldJson |> optional(field("label", string)));
  switch label {
  /* if label property is present, then compiler knows 100% we deal with metricWithLabel */
  | Some(v) => createCountWithLabel(v) |> MetricEncoder.encodeCountMetricWithLabel
  /* if label is absent, compiler knows, it deals with countMetric type */
  | None => countMetric |> MetricEncoder.encodeCountMetric
  }
};

I provided a simple source listing, but it comes from real production code. I have more 300 lines of code, which investigate ‘unknown’ data structure input and make it type proof during compilation and runtime times.

It might require some time to specify all possible types, variants, constructors for variants. But once you do it, you are 100% sure you covered all cases.

You can read my post on medium, how I dealt with wild input parameters here:


#4

Thanks for this, it helped me to grok the concept of type parameters and pass the typing off to the user of the component (who, theoretically, does know their data shape :grin:).


#5

My approach was to use a functor and allow the user provide the expected type. Type parameters are probably a better option