Typescript with React - 在通用组件类上使用 HOC

Posted

技术标签:

【中文标题】Typescript with React - 在通用组件类上使用 HOC【英文标题】:Typescript with React - use HOC on a generic component class 【发布时间】:2019-01-29 17:34:42 【问题描述】:

我有一个通用的 React 组件,像这样说:

class Foo<T> extends React.Component<FooProps<T>, FooState> 
    constructor(props: FooProps<T>) 
        super(props);

    render() 
        return <p> The result is SomeGenericFunction<T>()</p>;
    

我也有一个与这个类似的 HOC(但没那么无意义):

export const withTd = 
    <T extends WithTdProps>(TableElement: React.ComponentType<T>): React.SFC<T> => 
(props: T) => <td><TableElement ...props/></td>;

但是当我使用这样的组件时:

const FooWithTd = withTd(Foo);

没有办法传递类型参数,因为你既不能做withTd(Foo&lt;T&gt;),也不能做FooWithTd,类型总是错误的。 这样做的正确方法是什么?

编辑:问题是我希望稍后能够拥有类似 &lt;FooWithTd&lt;number&gt; ...someprops/&gt; 的东西,因为我不知道 HOC 中 T 的所需类型。

【问题讨论】:

可以进行编辑,但是您的 withTd 函数将 React.ComponentType&lt;T&gt; 作为参数,因此如果您想传递构造函数 Foo&lt;U&gt; 则 T 必须扩展 FooProps 或 WithTdProps 必须是可分配的到 FooProps 【参考方案1】:

编辑: 在对您的代码进行一些更改后,这只是您的 withTd 函数中的错误约束 T 。

// I needed to change the constraint on T, but you may adapt with your own needs
export const withTd = <T extends FooProps<WithTdProps>>(
  TableElement: React.ComponentType<T>
): React.SFC<T> => (props: T) => (
  <td>
    <TableElement ...props />
  </td>
)

// Explicitly typed constructor
// Removed after EDIT
//const FooW = Foo as new (props: FooProps<WithTdProps>) => Foo<WithTdProps>

// Inferred as React.StatelessComponent<FooProps<WithTdProps>>
const FooWithTd = withTd(Foo)

编辑后不再相关:

您可以在本期https://github.com/Microsoft/TypeScript/issues/3960找到更多信息

【讨论】:

【参考方案2】:

您可以将从 HOC 创建的组件包装到另一个组件中。它看起来像这样:

class FooWithTd<T> extends React.Component<SomeType<T>> 
     private Container: React.Component<SomeType<T> & HOCResultType>; 

     constructor(props:SomeType<T>)
          super(props);
          this.Container = withTd(Foo<T>);
     

     render() 
          return <this.Container ...this.props />;
     

请记住,您可能不希望在渲染函数中使用 HOC,因为这意味着每次渲染都会重新创建组件。

【讨论】:

【参考方案3】:

感谢您提出这个问题。我刚刚想出了一种在用 HOC 包装组件后为组件指定类型参数的方法,我想我会分享。

import React from 'react';
import withStyles from '@material-ui/core/styles/withStyles';
import  RemoveProps  from '../helpers/typings';

const styles = 
  // blah
;

interface Props<T> 
  classes: any;
  items: T[];
  getDisplayName: (t: T) => string;
  getKey: (t: T) => string;
  renderItem: (t: T) => React.ReactNode;


class GenericComponent<T> extends React.Component<Props<T>, State> 
  render() 
    const  classes, items, getKey, getDisplayName, renderItem  = this.props;

    return (
      <div className=classes.root>
        items.map(item => (
          <div className=classes.item key=getKey(item)>
            <div>getDisplayName(item)</div>
            <div>renderItem(item)</div>
          </div>
        ))
      </div>
    );
  


//   ? create a `type` helper to that output the external props _after_ wrapping it
type ExternalProps<T> = RemoveProps<Props<T>, 'classes'>;
export default withStyles(
  styles
)(GenericComponent) as <T extends any>(props: ExternalProps<T>) => any;
//                       ? cast the wrapped component as a function that takes
//                          in a type parameter so we can use that type
//                          parameter in `ExternalProps<T>`

主要思想是将包装的组件转换为接受类型参数(例如T)的函数,并使用该类型参数在组件被包装后派生外部道具.

如果你这样做,那么你可以在使用 GenericComponent 的包装版本时指定一个类型参数,例如:

<GenericComponent<string> /*...*/ />

希望代码对那些仍然有这个问题的人来说足够解释。不过,总的来说,我认为这种相对高级的打字稿用法可能更容易使用any 而不是道具中的通用参数

【讨论】:

如果您需要在函数中引用泛型类型,例如使用钩子 (useTable&lt;T&gt;),这可能行不通【参考方案4】:

也偶然发现了这一点,并认为我最终会分享我的想法。

基于@rico-kahler 提供的内容,我映射到您的代码的方法将是

export const FooWithTd = withTd(Foo) as <T>(props: FooProps<T>) => React.ReactElement<FooProps<T>>;

你可以像这样使用它

export class Bar extends React.Component<> 
  public render() 
    return (
      <FooWithTd<number> />
    );
  

就我而言,我也有defaultProps,并且我通过另一个 HOC 注入 props,更完整的解决方案如下所示:

type DefaultProps = "a" | "b";
type InjectedProps = "classes" | "theme";
type WithTdProps<T> = Omit<FooProps<T>, DefaultProps | InjectedProps> & Partial<FooProps<T> &  children: React.ReactNode >;
export const FooWithTd = withTd(Foo) as <T>(props: WithTdProps<T>) => React.ReactElement<WithTdProps<T>>;

【讨论】:

【参考方案5】:

解决方法:简单案例

如果您的组件的类型参数仅用于将其传递给道具,并且该组件的用户不希望它具有除了传递道具和渲染之外的任何功能,您可以将hoc(...args)(Component) 的结果显式硬转换为React 的函数式组件类型,像这样:

import React, ReactElement from 'react';

class MyComponent<T> extends React.Component<MyProps<T>>  /*...*/ 

const kindaFixed = myHoc(...args)(MyComponent) as unknown as <T>(props: MyProps<T>) => ReactElement;

解决方法:更复杂并且需要一些运行时成本

你可以使用类似织物的功能,假设here:

class MyComponent<T> extends React.Component<MyProps<T>>  /*...*/ 

export default function MyComponentFabric<T>() 
    return hoc(...args)(MyComponent as new(props: MyProps<T>) => MyComponent<T>);

这将要求您为使用它的每种类型创建新版本的包装组件:

import MyComponentFabric from '...whenever';

const MyComponentSpecificToStrings = MyComponentFabric<string>();

它将允许您访问组件的所有公共实例字段和方法。

总结

我在我的ExampleGenericComponent&lt;T&gt; 上尝试使用来自react-reduxconnect 时遇到了这个问题。不幸的是,在 TypeScript 支持 HKT 之前无法正确修复它,并且您使用的任何 HOC 都会根据此功能更新其类型。

当您需要访问组件实例字段和方法时,可能没有正确的解决方案(至少目前是这样)。 “正确”是指“没有丑陋的显式类型转换”和“没有运行时成本”。

您可以尝试的一件事是将您的类组件拆分为两个组件,一个将与 HOC 一起使用,另一个将提供您需要的字段和方法。

【讨论】:

以上是关于Typescript with React - 在通用组件类上使用 HOC的主要内容,如果未能解决你的问题,请参考以下文章

如何在 React with Typescript 中使用和指定通用 Hooks?

Typescript with React - 在通用组件类上使用 HOC

React Typescript with hooks:最大更新深度超出错误

Jest Manual Mocks with React 和 Typescript:模拟 ES6 类依赖

[React Native] Up & Running with React Native & TypeScript

yarn create react-app with typescript throwing errors