TypeScript 注释:选择、排除和高阶组件

2025-05-25

TypeScript 注释:选择、排除和高阶组件

介绍

这些说明应该有助于更好地理解,TypeScript并且在需要查找如何在特定情况下利用 TypeScript 时可能会有所帮助。所有示例均基于 TypeScript 3.2。

选择并排除

这些笔记主要关注 React 中高阶组件的类型,但理解起来也很有趣omitExclude因为我们需要这两个函数来处理不同的高阶组件 (hoc) 实现。顾名思义Pick,我们可以从提供的类型定义中选择特定的键。例如,我们可能正在使用对象展开,并希望选择特定属性并展开其余属性。让我们看下面的例子来更好地理解:

const { name, ...rest } = props;
Enter fullscreen mode Exit fullscreen mode

我们可能想在函数内部使用名称做一些事情,但只传递其余的道具。

type ExtractName = {
  name: string
}

function removeName(props) {
  const {name, ...rest} = props;
  // do something with name...
  return rest:
}
Enter fullscreen mode Exit fullscreen mode

让我们为函数添加类型removeName

function removeName<Props extends ExtractName>(
  props: Props
): Pick<Props, Exclude<keyof Props, keyof ExtractName>> {
  const { name, ...rest } = props;
  // do something with name...
  return rest;
}
Enter fullscreen mode Exit fullscreen mode

这里有很多事情要做,首先,我们扩展了泛型Props以包含 name 属性。
然后,我们提取了该name属性并返回了剩余的属性。为了告诉 TypeScript 我们泛型剩余类型的结构,我们需要删除所有 ExtractName 属性(在本例中为 name)。这就是它Pick<Props, Exclude<keyof Props, keyof ExtractName>>的作用。让我们进一步分解,以便更好地理解。Exclude删除特定的键:

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

Exclude<User, "id" | "registeredAt"> // removes id and registeredAt
Enter fullscreen mode Exit fullscreen mode

我们可以通过以下方式实现同​​样的目的Pick

Pick<User, "name" | "location">
Enter fullscreen mode Exit fullscreen mode

我们可以重写上述定义:

type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type Diff<T, K> = Omit<T, keyof K>;
Enter fullscreen mode Exit fullscreen mode

现在我们有了一个Diff函数,我们可以重写我们的removeName函数:

function removeName<Props extends ExtractName>(
  props: Props
): Diff<Props, ExtractName> {
  const { name, ...rest } = props;
  // do something with name...
  return rest;
}
Enter fullscreen mode Exit fullscreen mode

Pick我们应该对 hocs 的工作原理和功能有一个基本的了解,Exclude并且还要添加Omit以及Diff我们将在何时使用 hocs 类型,如下一节所示。

高阶组件

我们将参考React 官方文档,以便更好地理解一些约定,然后介绍不同的 hoc 变体。
我们需要考虑一个重要的约定:将不相关的 Props 传递给包装组件参见文档)。

我们的第一个示例基于文档中的示例,我们希望通过提供记录包装组件的组件来记录道具。

function withLogProps(WrappedComponent) {
  return class LogProps extends React.Component {
    componentWillReceiveProps(nextProps) {
      console.log("Currently available props: ", this.props);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

我们可以利用React.ComponentTypeReact 特有的类型,它允许我们将组件类或函数作为包装组件传入。由于我们没有在withLogProps高阶组件中扩展或缩小任何 props,因此我们可以传递通用 props。

function withLogProps<Props>(WrappedComponent: React.ComponentType<Props>) {
  return class LogProps extends React.Component<Props> {
    componentWillReceiveProps() {
      console.log("Currently available props: ", this.props);
    }
    render() {
      return <WrappedComponent {...this.props} />;
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

接下来,让我们看看如何输入一个高阶组件,该组件需要额外的道具来在发生错误时显示消息。

function withErrorMessage(WrappedComponent) {
  return function() {
    const { error, ...rest } = props;
    return (
      <React.Fragment>
        <WrappedComponent {...rest} />
        {error && <div>{error}</div>}
      </React.Fragment>
    );
  };
}
Enter fullscreen mode Exit fullscreen mode

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>
    );
  };
}
Enter fullscreen mode Exit fullscreen mode

这里有一些有趣的方面,我们需要澄清一下。
我们的 hoc 扩展了预期的 props,除了期望error包装组件所需的所有 props 之外,还可以期望一个,这可以通过将通用的包装组件 props 与所需的错误消息 prop 相结合来实现:Props & ErrorLogProps
另一个有趣的方面是,我们需要ErrorLogProps通过对解构的 props 进行类型转换来明确定义哪些 props 是:const { error, ...rest } = props as ErrorLogProps
在传递其余 props 时,TypeScript 仍然会报错,因此我们也需要对其余 props 进行类型转换:<WrappedComponent {...rest as Props} />。这在未来可能会发生变化,但在3.2中,这是防止 TypeScript 报错所必需的。

在某些情况下,我们希望为包装组件提供特定的功能和值,同时又防止这些功能和值被提供的 props 覆盖。
我们的下一个高阶组件应该缩小 API 的范围。

假设我们有一个Input组件,它期望

const Input = ({ value, onChange, className }) => (
  <input className={className} value={value} onChange={onChange} />
);
Enter fullscreen mode Exit fullscreen mode

高阶组件应该提供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}
        />
      );
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

让我们首先定义所需的道具类型。

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

type WithOnChangeProps = {
  onChange: (e: React.ChangeEvent<HTMLInputElement>) => void,
  value: string | boolean
};
Enter fullscreen mode Exit fullscreen mode

这意味着我们可以Input通过组合这些 prop 类型定义来定义我们的组件。

const Input = ({
  value,
  onChange,
  type,
  name
}: InputProps & WithOnChangeProps) => (
  <input type={type} name={name} value={value} onChange={onChange} />
);
Enter fullscreen mode Exit fullscreen mode

通过向组件添加类型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} // we need to be explicit here
          onChange={this.onChange}
          value={this.state.value}
        />
      );
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

通过使用我们之前定义的Diff类型,我们可以提取所有想要防止被覆盖的键。这使我们能够为Input组件提供onChangevalue属性。

const EnhancedInput = withOnChange(Input);

// JSX
<EnhancedInput type="text" name="name" />;
Enter fullscreen mode Exit fullscreen mode

在某些情况下,我们需要扩展 props,例如,我们希望允许开发者使用 propswithOnChange来提供初始值。我们可以重写组件,允许使用 props 来提供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}
        />
      );
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

这里有两点需要注意。我们OnChange通过定义 来扩展类 props Diff<Props, WithOnChangeProps> & ExpandedOnChangeProps,另一点很重要的一点是,我们必须initialValue从传递给包装组件的 props 中删除 。我们已经在最初的示例中看到了这一点,通过扩展通用 props 并删除initialValue

const { initialValue, ...props } = this.props as ExpandedOnChangeProps;

另一种可能的情况是,当我们想要定义一个通用组件时,我们可能需要提供一个高阶组件,该组件需要一个包装组件以及额外的配置或功能。让我们编写一个组件,它需要一个获取函数和一个组件,并返回一个组件,该组件根据获取的结果,可以显示任何内容、加载指示器、错误消息,或者在获取成功的情况下显示包装组件。

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} />;
      }
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

我们需要做一些工作来防止 TypeScript 报错。
我们可以做的第一件事是定义实际的组件状态:

type RemoteData<Error, Data> =
  | { type: "NotLoaded" }
  | { type: "Loading" }
  | { type: "Error", error: Error }
  | { type: "Success", data: Data };

type FetchState<Error, Data> = {
  data: RemoteData<Error, Data>
};
Enter fullscreen mode Exit fullscreen mode

withFetch我们可以定义组件在调用提供的函数时应该期望的承诺结果类型,这样我们就可以保证返回的承诺结果类型与我们包装组件中的预期数据属性相匹配。

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} />;
      }
    }
  };
}
Enter fullscreen mode Exit fullscreen mode

我们可以写更多的例子,但作为该主题的介绍,这些例子应该成为进一步研究该主题的基础。

如果您有任何问题或反馈,请在此处发表评论或通过 Twitter 联系:A. Sharif

文章来源:https://dev.to/busypeoples/notes-on-typescript-pick-exclude-and-higher-order-components-40cp
PREV
2024 年 8 大最佳 Visual Studio 扩展
NEXT
显然已经退休了