Gentype: Different representation in ReScript compared to generated TypeScript

I think I’m missing something very simple here. I’m trying to export some types from rescript to typescript.

Repo here: https://github.com/tom-sherman/rescript-xstate You just have to install the dependencies, build the rescript, then node src/XStateFunctor.bs.js

I have some rescript like this:

@genType
type rec stateNode<'state, 'event> =
  | State({name: 'state, on: array<('event, 'state)>})
  | Compound({name: 'state, children: array<stateNode<'state, 'event>>, initial: 'state})
  | Final({name: 'state})

@genType
type stateList<'state, 'event> = array<stateNode<'state, 'event>>

Which generates the following typescript:

export type stateNode<state,event> = 
    { tag: "State"; value: { readonly name: state; readonly on: Array<[event, state]> } }
  | { tag: "Compound"; value: {
  readonly name: state; 
  readonly children: stateNode<state,event>[]; 
  readonly initial: state
} }
  | { tag: "Final"; value: { readonly name: state } };

export type stateList<state,event> = stateNode<state,event>[];

However a stateNode isn’t of that shape when I pass it into JS-land, here’s an example of one stateNode object:

{ TAG: 0, name: 'idle', on: [ [ 'FETCH', 'loading' ] ] }

I think this is an issue with GenType. For all boxed variants, it adds a field based on whether it’s a polymorphic variant or not. See: https://github.com/rescript-association/genType/blob/master/src/EmitType.ml#L260-L263 and https://github.com/rescript-association/genType/blob/master/src/Runtime.ml#L78-L82.

It should be noted that the code that ReScript generates differs based on whether inline records are used or not, see https://github.com/rescript-association/genType/blob/master/src/EmitType.ml#L260-L263.

Given how dependent this is on ReScript’s code generation, I think merging GenType into ReScript itself might make sense.

1 Like

Thanks, I’ve raised it here: https://github.com/rescript-association/genType/issues/585

I’m sidestepping it for now by using polymorphic variants.

I believe this is because @genType needs to be added to both the type and the functions using that type. Wrapper functions are then generated to perform the conversion and map the ReScript implementation to the TypeScript type.

e.g. this code:

Generates:

1 Like

Hi,

Yeah the representation of variants in the generated javascript and the typescript are not same. But this would not break the app and is type safe.

So genType generated bindings are in such a way that it accepts the typescript type representation and then converts them to ReScript representation before passing to rescript functions.

For the following ReScript code

@genType
type rec stateNode<'state, 'event> =
  | State({name: 'state, on: array<('event, 'state)>})
  | Compound({name: 'state, children: array<stateNode<'state, 'event>>, initial: 'state})
  | Final({name: 'state})

@genType
let logStateNode = (a: stateNode<string, string>) => Js.log(a)

The generated typescript bindings are

/* TypeScript file generated from Test2.res by genType. */
/* eslint-disable import/first */


// @ts-ignore: Implicit any on import
import * as Test2BS__Es6Import from './Test2.bs';
const Test2BS: any = Test2BS__Es6Import;

// tslint:disable-next-line:interface-over-type-literal
export type stateNode<state,event> = 
    { tag: "State"; value: { readonly name: state; readonly on: Array<[event, state]> } }
  | { tag: "Compound"; value: {
  readonly name: state; 
  readonly children: stateNode<state,event>[]; 
  readonly initial: state
} }
  | { tag: "Final"; value: { readonly name: state } };

export const logStateNode: (a:stateNode<string,string>) => void = function (Arg1: any) {
  const result = 
/* WARNING: circular type stateNode. Only shallow converter applied. */
  Test2BS.logStateNode(Arg1.tag==="State"
    ? Object.assign({TAG: 0}, Arg1.value)
    : Arg1.tag==="Compound"
    ? Object.assign({TAG: 1}, Arg1.value)
    : Object.assign({TAG: 2}, Arg1.value));
  return result
};