【譯文】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} />;
      }
    }
  };
}
複製代碼

咱們還能夠寫不少的例子,可是做爲這個主題的第一篇文章,這些例子留着做爲更深刻的研究。

相關文章
相關標籤/搜索