rescript-association / genType

Auto generation of idiomatic bindings between Reason and JavaScript: either vanilla or typed with TypeScript/FlowType.

Home Page:https://rescript-lang.org/docs/gentype/latest/introduction

Geek Repo:Geek Repo

Github PK Tool:Github PK Tool

Consider using React.ReactNode instead of JSX.Element

a-gierczak opened this issue · comments

When generating TypeScript from Reason types I think Reason's React.element should be converted to React.ReactNode instead of JSX.Element. E.g. when component has label prop in RE which can be either an element or string you would write:

// label is React.element
<Comp label={React.string("label")} />
or
<Comp label={<div />} />

And in TS both are valid

// label is React.element
<Comp label="label" />
or
<Comp label={<div />} />

However first case (string as prop) won't work with JSX.Element type.

There's been some iteration on these types in the past. The type React.element can be used in a component that is exported or imported. Using a more general type makes the exported component more flexible, but imposes stronger constraints on imported components.

The concern is what happens in existing codebases, e.g. this would break imports of components already typed with JSX.Element.

I see. What about making it configurable via a shim or option in the bsconfig? That way it wouldn't break existing imports and allow using more idiomatic type when exporting to TS.

I'd like to avoid adding configurations if possible.
Wondering if there's a way to test what effect this change would have on people's codebases. That's the info I'm missing.
If there are no issues, I'd be happy to make the change. Though I'm not sure how to find out.

@a-gierczak happy to revisit this by running an experiment and see if the defaults can be changed. Not sure how to go about it and really check this would not break people.

I might be running into a problem because of this. I'm trying to use this component below. The generated .tsx has a type check error for the children property. If I change the .tsx children type from JSX.Element to React.ReactNode it works. I find this a bit confusing because I think the typed checked component I defined is a subset of the actual component; children of JSX.Element should be assignable to a children of React.ReactNode.

npm install @fluentui/react

module FluentMessageBar = {
  @genType.import("@fluentui/react") @react.component
  external make: (~children: React.element, unit) => React.element = "MessageBar"
}

// type check error - ReactNode is not assignable to type Element
export const MessageBarTypeChecked: React.ComponentType<{ readonly children: JSX.Element }> = MessageBarNotChecked;

A good summary of the different possible types for children: https://www.carlrippon.com/react-children-with-typescript/

This seems to support the change to ReactNode.

@jmagaram see the PR #579.
Feel free to test the npm tarball produced by CI in https://github.com/rescript-association/genType/pull/579/checks?check_run_id=3953777327 and see if it works.

I don't understand the scope of your proposed change. Are you using ReactNode everywhere or just for the children property?

Consider the following. Microsoft explicitly typed the actions parameter as a JSX.Element while the children property needs to be ReactNode. So if you use ReactNode EVERYWHERE this breaks.

module FluentMessageBar = {
  @genType.import("@fluentui/react") @react.component
  external make: (~children: React.element, ~actions: React.element=?, unit) => React.element =
    "MessageBar"
}

When I follow the TypeScript type definitions for type ComponentType<P = {}> = ComponentClass<P> | FunctionComponent<P> and follow both paths, I do eventually see the children property defined explicity as ReactNode. On the functional path I see type PropsWithChildren<P> = P & { children?: ReactNode };. So it seems safe to use ReactNode for the children property.

But for any other property, it seems risky to swap JSX.Element for ReactNode.

My workaround for this problem was to define my own partial definition of ReactNode like this. But this is a hassle to work with because every time I need a ReactNode I've got to explicitly create it. When a children property was needed, I couldn't just use regular JSX syntax and instead had to write my children properties like {...element... -> ReactNode.fromElement}. If you fix this situation for the children property that would be great. I'm having trouble getting my head around a more general solution.

module ReactNode = {
  @gentype.import(("react", "ReactNode"))
  type t

  external fromElement: React.element => t = "%identity"
  external fromString: string => t = "%identity"
  external fromBool: bool => t = "%identity"
}

@jmagaram indeed typing everything with ReactNode would be problematic.
Here's a PR #580 that caters specifically for children of imported components. The case of children for exported components was already handled.
One curious behaviour of things such as FunctionComponent is that even though JSX.Element can be converted to React.ReactNode, the subtype does not apply when instantiating the props parameters of FunctionComponent. That means that one cannot have a type to cover all cases, and the best one can do is try to cover the most typical case. With this change, the assumption would be that the most common case for children is ReactNode.

I tried it by replacing the .exe in the node_modules\gentype folder. I'm not sure it is working as you expect. This import...

  @genType.import("@fluentui/react") @react.component
  external make: (
    ~messageBarType: barType=?,
    ~children: React.element,
    ~actions: React.element=?,
    ~isMultiline: bool=?,
    unit,
  ) => React.element = "MessageBar"

...turns into this generated code, which has a type check error on the actions property because that is supposed to be no wider than a JSX.Element.

export const MessageBarTypeChecked: React.ComponentType<{
  readonly messageBarType?: FluentMessageBar_barType; 
  readonly children: React.ReactNode; 
  readonly actions?: React.ReactNode; 
  readonly isMultiline?: boolean
}> = MessageBarNotChecked;

Also, and I realize this should be expected, my workaround code broke. I had written a wrapper for the React component that took in a JSX.Element and converted it to a ReactNode. I easily fixed this by changing the children to ReactNode but better by removing my wrapper function entirely.

  create: (p: {
    messageBarType?: MessageBarType;
    children: JSX.Element;
    isMultiline?: boolean;
    onDismiss?: () => void;
  }) => {
    const dismiss = p.onDismiss;
    let props: IMessageBarProps = {
      isMultiline: p.isMultiline,
      children: p.children,
      onDismiss: dismiss ? (_) => dismiss() : undefined,
      messageBarType: p.messageBarType,
    };
    return <MessageBar {...props} />;
  },

In release 4.3.0.