原文地址: 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} />;
}
}
};
}
複製代碼
咱們還能夠寫不少的例子,可是做爲這個主題的第一篇文章,這些例子留着做爲更深刻的研究。