RFC: More general type checking for structural typings

This would allow for pretty massive changes to the way JSX works. Today <Foo prop1=1 /> abstractly compiles to jsx(Foo.make, Foo.makeProps(~prop1=1, ())). With this change it could compile to something more like jsx(Foo.make, {"prop1": 1}). This means that React components are just one value instead of make and makeProps. It would greatly simplify the @react.component part of the jsx transform to the point where really the only thing it does is provide component names and handle default props.

19 Likes

Just out of curiosity, do you see components still using labelled arguments (as opposed to an object)? Seems like labelled args would provide better ergonomics (assuming no object destructuring syntax… which maybe isn’t a safe assumption? :smiley: )

Labelled arguments are still going to be used, the underlying transformation would be improved.

Edit: this is referring to the call site, not declaration.

1 Like

We’ll see! =)
The goal is to have as little of a ppx transform as possible, ideally none, since even a tiny ppx vs no ppx is still a world of difference in terms of tooling support and impedance mismatch.

4 Likes

I have another approach to solve this use case (already an incorrect POC made) using nominal types.
So you can do something like this:

type config = { 
   x : int ,
   y : option < int >,
   z : option < int> 
}
let fn = (c : config) => {
   ...
}
fn ({x : 3 }) // The type system allows optional labels to be initialized with None for optional types

Does this cover your use case? This feature alone is useful in other scenarios?

12 Likes

This approach for record types would be great! Even if the JS output is fn({ x: 3, y: undefined, z: undefined}) (to preserve performance predictability), this is a great win for usability of record types with lots of optional fields.

1 Like

Yeah that solution would be really nice @Hongbo !

4 Likes

I think we can make it configurable per type, so that user can choose do display with undefined or not, we will check how much effort it needs to be done

3 Likes

That would be amazing @Hongbo. I think a lot of libraries already have these records (I have a lot of bindings that look like that), so with this improvmeent we can just drop the factory functions with optional labeled arguments.

4 Likes

Hi all,
I believe most technical challenging problems are solved with this proposal,
we can have lots of innovations with nominal typed records.
Would be happy to hear if you have some more innovative ideas. (Note this is a non breaking enhancement)

Here is a complex demo for preview:

type t0 = {
  x : int ,
  y : option<string>
}

let h = { x : 3 }
module N = {
  type c = option<string>
}

module Make = () => {
  type t<'a> = {
    x: int,
    @as("Y") y: option<string>,
    z: option<'a>,
    h: N.c,
  }
}

module N0 = Make()
open N0

let f = {
  x: 3,
}

Generated JS code:

'use strict';


var N = {};

function Make($star) {
  return {};
}

var N0 = {};

var h = {
  x: 3,
  y: undefined
};

var f = {
  x: 3,
  Y: undefined,
  z: undefined,
  h: undefined
};
18 Likes

Great to hear that this proposal is making good progress. :+1:

However, considering that config objects or React props objects can have a huge number of (optional) fields, I think the generated code should just omit the optional fields instead of setting them to undefined.

Otherwise this would not really be a viable replacement for @obj for these use cases IMHO.

3 Likes

It would be great to “compress” the js object in some cases, but it also has some performance implications, and it would be a breaking change. So I think this should be a separate proposal that is opt-in per record with some kind of annotation.

However this is is already great for most existing use-cases, also amazing that it even works in advanced cases like when using functors and type aliases, shows the power of the type checker! Thanks for this incredible work @Hongbo!

7 Likes

This is fantastic I even see this leading to a better interop for React components especially when you want to reuse type definitions

// NewDiv.res
let make = ({ a, b, style }: {
 ...ReactDOM.baseProps, 
 a: int,
 b: int
}) => ...

:eyes:

1 Like

I came across a situation lately that may be related.
We are writing some bindings for Highcharts and found that their config inputs behave differently with a field with undefined value, and an undefined field. The undefined field was their preference and it crashed pretty gruesomely with { value: undefined } I didnt look deeply but assume they are doing something by Object.keys/hasKey and not checking the resulting value.

I would be surprised if much code behaves the other way where it expects the key to exist and be undefined but I wouldnt be so surprised. Anyways point being that precise control of the underlying structure can and does come up!

Alex

1 Like

I’ve ran into similar requirements for other libraries, fwiw @obj appears to address those use cases.

1 Like

Obj does work but produces an inspecific type which is gruesome for a deep hierarchy of configs (Highcharts clocking in at about 20k lines so far).
I preferred @deriving(abstract).

1 Like

I agree with you that’s probably the main use case for lots of optional properties. So if we only expose such functionality:

@obj
type t = {
   x : int,
   y0 : option<int>,
  ..
  yN : option<string>
}
let h : t = {x }

which generates code like this:

var h = { x }

Would this solve most use cases?
In that case, this feature would be opt-in for an obj style record only.
Do we have more use cases beyond that pattern?

5 Likes

I would also check if this would completely address all the use cases of deriving(abstract)?

5 Likes

Yes, for me as a bindings author, I could add an @obj annotation to the record type if required, and that would solve my use cases if the code gets compiled as you described.

However, my feeling is that this should actually be the default behavior. I think newcomers coming from JS/TS would definitely expect

let h: t = { x: 42 }

to be compiled to

var h = { x: 42 }

and not

var h = { x: 42, y0: undefined, ... yN: undefined }

Would this really be a breaking change/affect existing code? Would fields that are explicitly set to None also be omitted?

1 Like

That would definitely remove the need for deriving abstract or @obj for me as a binding author!

1 Like