9d1f8cf6d797da197777be2e6ea05f06
TypeScript React 高阶组件

前言

本文是对 React Higher-Order Components in TypeScript 这篇文章的翻译与学习。

React 高阶组件(HOC)是组件间代码重用的强力工具。对于用 TypeScript 的开发者来说,为了弄明白类型常常搞得他们很头疼。

这篇文章假设已经熟悉高阶组件(HOC)。在本文中,将会通过一个由浅入深的例子,来展示怎么样搞定 TypeScript 高阶组件,避免用 any 来糊弄过去。

为此,高阶组件在本文中被拆分为两种基本模式,分别起了一个名字:

  • Enhancers:对一个组件进行封装,附加功能或者 Props
  • Injectors:将 Props 注入到组件中

一个高阶组件总能够归为以上两类中的其一,在下文的示例中将会说明。

注意,这篇文章的目的是展示怎么正确给 HOCs 设置类型,而不是为了展示最佳实践,例如 add a display namehoisting statics

Enhancers

我们首先从 Enhancers 开始,因为它的类型最容易。给出应用这个模式的一个基本示例是:向一个组件中加入一个 loading prop,如果设为 true 的话,展示一个 loading spinner。

不带类型的示例代码如下:

const withLoading = Component =>
  class WithLoading extends React.Component {
    render() {
      const { loading, ...props } = this.props;
      return loading ? <LoadingSpinner /> : <Component {...props} />;
    }
  };

。。。做好心理准备。。。

带类型的示例代码如下:

interface WithLoadingProps {
  loading: boolean;
}

const withLoading = <P extends object>(Component: React.ComponentType<P>) =>
  class WithLoading extends React.Component<P & WithLoadingProps> {
    render() {
      const { loading, ...props } = this.props as WithLoadingProps;
      return loading ? <LoadingSpinner /> : <Component {...props} />;
    }
  };

这一大坨东西……下面拆开来看:

interface WithLoadingProps {
  loading: boolean;
}

这个接口给出的是在原有组件基础上新添加的 props,这也是对原有组件增强(enhance)的地方。

<P extends object>(Component: React.ComponentType<P>)

这里我们使用了一个泛型。其中,P 表示传入 HOC 的组件 Props。React.ComponentType<P>React.StatelessComponent<P> | React.ClassComponent<P> 的一个 alias,意思是传入 HOC 的组件可以是一个无状态的函数式组件,也可以是一个类组件。

class WithLoading extends React.Component<P & WithLoadingProps>

这里我们定义了 HOC 返回的组件,并且指定组件的 Props 包含传入的 Props(P)和 HOC 的 Props(WithLoadingProps)。这里通过 type intersection 符(&)对两种类型进行了组合。

const { loading, ...props } = this.props as WithLoadingProps;

之后从 HOC 的 props 里面拿出 loading,并把剩下的都放在 props 里面。你会注意到,这里用到了一个类型 cast(as WithLoadingProps),这是因为 TypeScript object rest/spread 的一个问题,目前正在解决中

总之,类型 cast 是坏事,但是这里没办法,只能这样做,要是不这样,就报错:

[ts] Rest types may only be created from object types.

最终,我们使用解析出的 loading props 来进行条件判断,是返回 loading spinner 还是返回组件:

return loading ? <LoadingSpinner /> : <Component {...props} />;

我们的 withLoading HOC 还可以重写,改成返回一个无状态的函数式组件,而不是返回一个类,具体写法为:

const withLoading = <P extends object>(
  Component: React.ComponentType<P>
): React.SFC<P & WithLoadingProps> => ({
  loading,
  ...props
}: WithLoadingProps) =>
  loading ? <LoadingSpinner /> : <Component {...props} />;

这里,我们同样有 object rest/spread 问题,通过明确指定返回值类型 React.SFC<P & WithLoadingProps> 给解决了。在无状态的函数式组件里,我们实际使用的还是 WithLoadingProps

注意:React.SFCReact.StatelessComponent 的简写。

Injectors

Injectors 是更普遍的一种 HOC 形式,但是设置类型更难。

除了向组件中注入 Props,大多数情况还能像被封装的组件中写死一些 Props,让外界传入的对应 Props 失效。react-redux 的 connect 就是一个 injector HOC 的例子,但是在本文中,我们使用一个更简单的例子——一个 HOC 注入一个计数值和两个回调(increment 和 decrement):

```ts
import { Subtract } from 'utility-types';

export interface InjectedCounterProps {
value: number;
onIncrement(): void;
onDecrement(): void;
}

interface MakeCounterState {
value: number;
}

const makeCounter =

(
Component: React.ComponentType


) =>
class MakeCounter extends React.Component< Subtract,
MakeCounterState

{
state: MakeCounterState = {
value: 0,
};

increment = () => {
  this.setState(prevState => ({
    value: prevState.value + 1,
  }));
};

decrement = () => {
  this.setState(prevState => ({
    value: prevState.value - 1,
  }));
};

render() {
  return (
    <Component
      {...this.props}
top Created with Sketch.