【译文】TypeScript笔记1/17:Pick,Exclude与高阶组件

原文地址: Notes on TypeScript: Pick, Exclude and Higher Order Components
本系列文章共17篇,此为第1篇javascript

引言

这些笔记有助于更好的理解TypeScript,并能够用来查询特殊状况下的TypeScript使用。例子基于TypeScript 3.2。html

Pick与Exclude

本文主要阐述如何编写React中的高阶组件hoc。首先为了处理不一样的hoc实现问题,咱们须要理解omit和exclude这两个函数。pick能够用来从已定义的类型中挑选特定的键值keys。例如,咱们可能想使用对象扩展运算符来选取特定的属性,并扩展剩下的属性,例如:java

const { name, ...rest } = props;
复制代码

咱们可能想要用在函数里面使用name属性,并透传剩下的属性:react

type ExtractName = {
  name: string
}

function removeName(props) {
  const {name, ...rest} = props;
  // do something with name...
  return rest:
}
复制代码

如今给removeName函数加入类型:typescript

function removeName<Props extends ExtractName>( props: Props ): Pick<Props, Exclude<keyof Props, keyof ExtractName>> {
  const {name, ...rest} = props;
  // do something with name...
  return rest:
}
复制代码

上面的例子作了不少事情,它先是继承了Props来包含name属性。而后抽取了name属性,并返回剩下的属性。为了告诉TypeScript函数返回类型的结构,咱们移除了ExtractName中的属性(这个例子中的name)。这些工做都是 Pick<Props, Exclude<keyof Props, keyof ExtractName>> 实现的。为了更好的理解,让咱们更深刻的研究下去。 Exclude 移除了特定的keys:promise

type User = {
  id: number;
  name: string;
  location: string;
  registeredAt: Date;
};

Exclude<User, "id" | "registeredAt"> // removes id and registeredAt
复制代码

能够用 Pick 实现相同的功能:app

Pick<User, "name" | "location">
复制代码

重写上面的定义:函数

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type Diff<T, K> = Omit<T, keyof K>;
复制代码

用Diff函数重写removeName函数:fetch

function removeName<Props extends ExtractName>( props: Props ): Diff<Props, ExtractName> {
    const { name, ...rest } = props;
    // do something with name...
    return rest;
}
复制代码

到此,咱们对于文章后半部分书写hoc将会用到的函数(PickExcludeOmitDiff)有了一个初步的了解。ui

高阶组件,Higher Order Component (HOC)

咱们能够查阅 React官方文档 来了解,讨论和书写不一样的hoc组件。 这部分咱们将讨论如何透传不相关的属性props给wrapped component(参考文档)。

第一个例子来自于官方文档,主要是想打印传给组件的props:

function withLogProps(WrappedComponent) {
    return class LogProps extends React.Component {
        componentWillReceiveProps(nextProps) {
            console.log('Currently available props: ', this.props)
        }
        render() {
            return <WrappedComponent {...this.props} /> } } } 复制代码

咱们能够利用React的特定类型 React.ComponentType 做为wrapped component的参数类型。withLogProps高阶组件既没有扩展也没有减小任何的props,只是透传了所有的props:

function withLogProps<Props>(WrappedComponent: React.ComponentType<Props>) {
    return class LogProps extends React.Component<Props> {
        componentWillReceiveProps(nextProps) {
            console.log('Currently available props: ', this.props)
        }
        render() {
            return <WrappedComponent {...this.props} /> } } } 复制代码

接下来,咱们再看一个高阶组件的列子,这个例子会接收额外的props以便发生错误时展现提示信息:

function withErrorMessage(WrappedComponent) {
    return function() {
        const { error, ...rest } = props;
        
        return (
              <React.Fragment>
                <WrappedComponent {...rest} />
                {error && <div>{error}</div>}
              </React.Fragment>
        );
      };
}
复制代码

withErrorMessage 和前面的例子很类似:

function withErrorMessage<Props>(WrappedComponent: React.ComponentType<Props>) {
  return function(props: Props & ErrorLogProps) {
    const { error, ...rest } = props as ErrorLogProps;
    return (
      <React.Fragment>
        <WrappedComponent {...rest as Props} />
        {error && <div>{error}</div>}
      </React.Fragment>
    );
  };
}
复制代码

这个例子有不少有意思的地方须要说明。

withErrorMessage hoc除了接收wrapped component须要的props以外还要接收 error 属性,这是经过组合wrapped component的props和error属性实现的: Props & ErrorLogProps

另外一个有趣的地方是,咱们须要显示强制转换构造的props为ErrorLogProps类型:const { error, ...rest } = props as ErrorLogProps

TypeScript仍然要解释剩下的属性,因此咱们也要强制转换剩下的属性:<WrappedComponent {...rest as Props} />。这个解释过程可能会在未来改变,可是TypeScript 3.2版本是这样子的。

某些状况下,咱们须要给wrapped component提供特定的功能和值,而这些功能和值不该该被外部传入的属性所覆盖。

接下来的hoc组件应该减小API。

假设有以下的Input组件:

const Input = ({ value, onChange, className }) => (
  <input className={className} value={value} onChange={onChange} /> ); 复制代码

hoc组件应该提供 valueonChange 属性:

function withOnChange(WrappedComponent) {
  return class OnChange extends React.Component {
    state = {
      value: ""
    };
    onChange = e => {
      const target = e.target;
      const value = target.checked ? target.checked : target.value;
      this.setState({ value });
    };
    render() {
      return (
        <WrappedComponent {...this.props} onChange={this.onChange} value={this.state.value} /> ); } }; } 复制代码

首先定义属性类型:

type InputProps = {
  name: string,
  type: string
};

type WithOnChangeProps = {
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
  value: string | boolean
};
复制代码

组合这些属性类型定义来定义 Input 组件:

const Input = ({
  value,
  onChange,
  type,
  name
}: InputProps & WithOnChangeProps) => (
  <input type={type} name={name} value={value} onChange={onChange} /> ); 复制代码

利用到目前为止学到的知识给 withOnChange 组件增长类型:

type WithOnChangeState = {
    value: string | boolean;
}

function withOnChange<Props>(WrappedComponent: React.ComponentType<Props>) {
  return class OnChange extends React.Component<Diff<Props, WithOnChangeProps>, WithOnChangeState> {
    state = {
      value: ""
    };
    onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
      const target = event.target;
      const value = target.type === 'checkbox' ? target.checked : target.value;
      this.setState({ value });
    };
    render() {
      return (
        <WrappedComponent {...this.props as Props} onChange={this.onChange} value={this.state.value} /> ); } }; } 复制代码

以前定义的 Diff 类型能够抽取不想被重写的keys。这样子 withOnChange高阶组件就能够给 Input 组件提供 onChangevalue 属性了:

const EnhancedInput = withOnChange(Input);

// JSX
<EnhancedInput type="text" name="name" />;
复制代码

某些状况下,咱们须要扩展属性。例如,让开发者使用withOnChange的时候能够提供一个初始值。增长一个 initialValue 属性来重写组件:

type ExpandedOnChangeProps = {
  initialValue: string | boolean;
};

function withOnChange<Props>(WrappedComponent: React.ComponentType<Props>) {
  return class OnChange extends React.Component<Diff<Props, WithOnChangeProps> & ExpandedOnChangeProps, WithOnChangeState> {
    state = {
      value: this.props.initialValue
    };
    onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
      const target = event.target;
      const value = target.type === 'checkbox' ? target.checked : target.value;
      this.setState({ value });
    };
    render() {
      const { initialValue, ...props } = this.props as ExpandedOnChangeProps;
      return (
        <WrappedComponent {...props as Props} // we need to be explicit here onChange={this.onChange} value={this.state.value} /> ); } }; } 复制代码

这里须要注意两处有意思的地方。首先,咱们经过定义Diff<Props, WithOnChangeProps> & ExpandedOnChangeProps,扩展了OnChange属性。其次,咱们必须先从属性里面移除initialValue,而后再传递给wrapped component:

const { initialValue, ...props } = this.props as ExpandedOnChangeProps;
复制代码

另外一个可能的场景是,定义一个能够接收wrapped component、额外的配置或其余功能的通用高阶组件。让咱们写一个能够接收fetch函数和wrapped component,并返回一个依赖fetch结果而渲染不一样东西的组件,渲染结果多是什么都不渲染,能够是一个loading,能够是出错信息,也能够是一个fetch成功的wrapped component:

function withFetch(fetchFn, WrappedComponent) {
  return class Fetch extends React.Component {
    state = {
      data: { type: "NotLoaded" }
    };
    componentDidMount() {
      this.setState({ data: { type: "Loading" } });
      fetchFn()
        .then(data =>
          this.setState({
            data: { type: "Success", data }
          })
        )
        .catch(error =>
          this.setState({
            data: { type: "Error", error }
          })
        );
    }
    render() {
      const { data } = this.state;
      switch (data.type) {
        case "NotLoaded":
          return <div />;
        case "Loading":
          return <div>Loading...</div>;
        case "Error":
          return <div>{data.error}</div>;
        case "Success":
          return <WrappedComponent {...this.props} data={data.data} />;
      }
    }
  };
}
复制代码

想要阻止TypeScript报错还有一些工做要作。首先是定义真正的组件state:

type RemoteData<Error, Data> =
  | { type: "NotLoaded" } // (译者注:这行行首的 | 有问题吧?)
  | { type: "Loading" }
  | { type: "Error", error: Error }
  | { type: "Success", data: Data };

type FetchState<Error, Data> = {
  data: RemoteData<Error, Data>
};
复制代码

咱们能够定义一个promise的结果类型,这个类型是withFetch组件想要提供给fetch函数的,这样子能够保证promise返回的结果类型与wrapped component所指望的data属性类型一致:

function withFetch<FetchResultType, Props extends { data: FetchResultType }>(
  fetchFn: () => Promise<FetchResultType>,
  WrappedComponent: React.ComponentType<Props>
) {
  return class Fetch extends React.Component<
    Omit<Props, "data">,
    FetchState<string, FetchResultType>
  > {
    state: FetchState<string, FetchResultType> = {
      data: { type: "NotLoaded" }
    };
    componentDidMount() {
      this.setState({ data: { type: "Loading" } });
      fetchFn()
        .then(data =>
          this.setState({
            data: { type: "Success", data }
          })
        )
        .catch(error =>
          this.setState({
            data: { type: "Error", error }
          })
        );
    }
    render() {
      const { data } = this.state;
      switch (data.type) {
        case "NotLoaded":
          return <div />;
        case "Loading":
          return <div>Loading...</div>;
        case "Error":
          return <div>{data.error}</div>;
        case "Success":
          return <WrappedComponent {...this.props as Props} data={data.data} />;
      }
    }
  };
}
复制代码

咱们还能够写不少的例子,可是做为这个主题的第一篇文章,这些例子留着做为更深刻的研究。

相关文章
相关标签/搜索