具有withStyles的JSX元素中的泛型类型参数

时间:2018-09-29 10:59:35

标签: reactjs typescript material-ui

在带有material-ui的React中,我试图创建一个JSX组件,该组件接受通用参数,并且还使用withStyles HOC注入我的样式。

第一种方法是这样的:

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>> {
      ...
    }
),

但是当尝试使用它时,通用类型会丢失

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

我唯一能找到的解决方案是将导出文件包装在一个具有通用参数并构造组件的函数中。

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);
}

尽管这只是在试图解决类型问题,但它却极其复杂,甚至带来运行时成本。

必须有一种更好的方法来使用具有通用参数和HOC的JSX组件。

这与https://github.com/mui-org/material-ui/issues/11921处的问题密切相关,但是从来没有令人满意的解决方案,并且此问题现已解决。

4 个答案:

答案 0 :(得分:3)

我对问题的思考越多,我就越喜欢Frank Li's approach。我将做两个修改:(1)引入一个额外的SFC以避免强制转换,(2)从包装的组件C中获取外部props类型,而不是对其进行硬编码。 (如果我们对Props<T>进行硬编码,TypeScript至少会检查它是否与this.C兼容,但是我们冒着需要this.C实际上并没有要求或没有接受this.C实际上接受的可选道具。)令人-目结舌的是,从extends子句中的类型参数引用属性类型是可行的,但似乎可以!

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} />;
  }
}

我认为,在整个React应用程序的上下文中,任何对这种方法的运行时开销的抱怨可能都是胡说八道。当有人提供支持他们的数据时,我会相信他们。

请注意,Lukas Zech使用SFC的方法非常不同:每次更改外部SFC的属性并再次调用它时,都会再次调用withStyles,生成wrapper,看起来像React就像全新的组件类型一样,因此React丢弃了旧的wrapper实例,并创建了一个新的内部BaseFormCard组件。这将具有不良的行为(重置状态),更不用说更大的运行时开销了。 (我实际上尚未对此进行测试,所以请告知我是否缺少任何内容。)

答案 1 :(得分:1)

这是不创建包装器组件的解决方案。相反,它创建的类型类似于组件类型,只是没有'classes'属性。

   // TypeUtils.tsx
   // from: https://stackoverflow.com/questions/48215950/exclude-property-from-type
   export type Omit<T, K extends keyof T> = Pick<T, Exclude<keyof T, K>>

使用此帮助程序类型,您可以创建所需的类型并将样式化的组件转换为该类型

   type FixedCheckBoxType = <T>(props: Omit<FormCheckBoxProps<T>, 'classes'>) => JSX.Element;
   export const FormCheckBox = withStyles(checkboxStyles)(UnstyledFormCheckBox) as FixedCheckBoxType;

可能会有更好的方法来执行此操作,但理想情况下,这将由material-ui本身自动完成。

答案 2 :(得分:1)

这对于使用Visual Studio的我来说已经足够了:

import React from "react";
import { withStyles, WithStyles } from "@material-ui/styles";

const styles = {
  root: { ... },
};

export interface Props<T> { ... }
export interface State<T> { ... }

export class TableComponent<T> extends React.PureComponent<Props<T> & WithStyles<typeof styles>, State<T>> {
  render () {
    return <div className={this.props.classes.root} />;
  }
}

export const Table = (withStyles(styles)(TableComponent) as any) as new <T>() => TableComponent<T>;

export default Table;

答案 3 :(得分:0)

对于每种可能数量的类型参数,withStyles类型的输入都必须重载