Generic type arguments in JSX elements with withSt

2019-02-19 10:38发布

问题:

In React with material-ui I am trying to create a JSX component that accepts generic parameters and also uses the withStyles HOC to inject my styles.

The first approach was like this:

const styles = (theme: Theme) => createStyles({
  card:    {
    ...
  }
});

interface Props<T> {
  prop: keyof T,
  ...
}

type PropsWithStyles<T> = Props<T> & WithStyles<typeof styles>;

export default withStyles(styles)(
    class BaseFormCard<T> extends React.Component<PropsWithStyles<T>> {
      ...
    }
),

But when trying to use this, the generic types are lost

<BaseFormCard<MyClass> prop={ /* no typings here */ } />

The only solution I could find was to wrap the export in a function which takes the generic parameter and constructs the component.

export default function WrappedBaseFormCard<T>(props: Props<T>): ReactElement<Props<T>> {

  const wrapper = withStyles(styles)(
        class BaseFormCard<T> extends React.Component<PropsWithStyles<T>> {
          ...
        }
    ) as any;

  return React.createElement(wrapper, props);
}

However this is ridiculously complicated and even comes with runtime cost, although it is only trying to solve problems typings.

There has to be a better way to use JSX components with generic parameters and HOCs.

This is closely related to the issue here https://github.com/mui-org/material-ui/issues/11921, but there was never satisfying solution and the issue is now closed.

回答1:

The more I think about the question, the more I like Frank Li's approach. I'd make two modifications: (1) introduce an extra SFC to avoid a cast, and (2) grab the outer props type from the wrapped component C instead of hard-coding it. (If we hard-coded Props<T>, TypeScript would at least check that it is compatible with this.C, but we are at risk of requiring props that this.C doesn't actually require or failing to accept optional props that this.C actually accepts.) It's jaw-dropping that referencing a property type from a type argument in the extends clause works, but it seems to!

class WrappedBaseFormCard<T> extends React.Component<
  // Or `PropsOf<WrappedBaseFormCard<T>["C"]>` from @material-ui/core if you don't mind the dependency.
  WrappedBaseFormCard<T>["C"] extends React.ComponentType<infer P> ? P : never,
  {}> {
  private readonly C = withStyles(styles)(
    // JSX.LibraryManagedAttributes handles defaultProps, etc.  If you don't
    // need that, you can use `BaseFormCard<T>["props"]` or hard-code the props type.
    (props: JSX.LibraryManagedAttributes<typeof BaseFormCard, BaseFormCard<T>["props"]>) =>
      <BaseFormCard<T> {...props} />);
  render() {
    return <this.C {...this.props} />;
  }
}

I think any complaints about runtime overhead of this approach are probably nonsense in the context of a whole React application; I'll believe them when someone presents data supporting them.

Note that Lukas Zech's approach using an SFC is very different: every time the props to the outer SFC change and it is called again, withStyles is called again, generating a wrapper that looks to React like a whole new component type, so React throws away the old wrapper instance and a new inner BaseFormCard component gets created. This would have undesirable behavior (resetting state), not to mention greater runtime overhead. (I haven't actually tested this, so let me know if I'm missing something.)



回答2:

There needs to be an overload of withStyles typings for every possible number of type parameters