原文地址: Notes on TypeScript: Pick, Exclude and Higher Order Components
本系列文章共17篇,此为第1篇javascript
这些笔记有助于更好的理解TypeScript,并能够用来查询特殊状况下的TypeScript使用。例子基于TypeScript 3.2。html
本文主要阐述如何编写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将会用到的函数(Pick
、 Exclude
、 Omit
、 Diff
)有了一个初步的了解。ui
咱们能够查阅 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} />;
}
}
};
}
复制代码
咱们还能够写不少的例子,可是做为这个主题的第一篇文章,这些例子留着做为更深刻的研究。