原文地址:
Notes on TypeScript: Pick, Exclude and Higher Order Components
本系列文章共17篇,此为第1篇
引言
这些笔记有助于更好的理解TypeScript,并可以用来查询特殊情况下的TypeScript使用。例子基于TypeScript 3.2。
Pick与Exclude
本文主要阐述如何编写React中的高阶组件hoc。首先为了处理不同的hoc实现问题,我们需要理解omit和exclude这两个函数。pick可以用来从已定义的类型中挑选特定的键值keys。例如,我们可能想使用对象扩展运算符来选取特定的属性,并扩展剩下的属性,例如:
const { name, ...rest } = props;
我们可能想要用在函数里面使用name属性,并透传剩下的属性:
type ExtractName = {
name: string
}
function removeName(props) {
const {name, ...rest} = props;
// do something with name...
return rest:
}
现在给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:
}
上面的例子做了很多事情,它先是继承了Props来包含name属性。然后抽取了name属性,并返回剩下的属性。为了告诉TypeScript函数返回类型的结构,我们移除了ExtractName中的属性(这个例子中的name)。这些工作都是 Pick<Props, Exclude<keyof Props, keyof ExtractName>> 实现的。为了更好的理解,让我们更深入的研究下去。 Exclude 移除了特定的keys:
type User = {
id: number;
name: string;
location: string;
registeredAt: Date;
};
Exclude<User, "id" | "registeredAt"> // removes id and registeredAt
可以用 Pick 实现相同的功能:
Pick<User, "name" | "location">
重写上面的定义:
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;
type Diff<T, K> = Omit<T, keyof K>;
用Diff函数重写removeName函数:
function removeName<Props extends ExtractName>(
props: Props
): Diff<Props, ExtractName> {
const { name, ...rest } = props;
// do something with name...
return rest;
}
到此,我们对于文章后半部分书写hoc将会用到的函数(Pick 、 Exclude、 Omit 、 Diff)有了一个初步的了解。
高阶组件,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组件应该提供 value 和 onChange 属性:
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 组件提供 onChange 和 value 属性了:
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} />;
}
}
};
}
我们还可以写很多的例子,但是作为这个主题的第一篇文章,这些例子留着作为更深入的研究。