原文連接: https://medium.com/@jrwebdev/...
高階組件(HOCs)在React中是組件複用的一個強大工具。可是,常常有開發者在結合TypeScript使用中抱怨道很難去爲其設置types。javascript
這邊文章將會假設你已經具有了HOCs的基本知識,並會根據由淺入深的例子來向你展現如何去爲其設置types。在本文中,高階組件將會被分爲兩種基本模式,咱們將其命名爲enhancers和injectors:html
請注意,本文中的示例並非最佳實踐,本文主要只是展現如何在HOCs中設置types。java
咱們將從enhancers開始,由於它更容易去設置types。此模式的一個基本示例是一個向組件添加loading props的HOC,而且將其設置爲true的時候展現loading圖。下面是一個沒有types的示例:react
const withLoading = Component => class WithLoading extends React.Component { render() { const { loading, ...props } = this.props; return loading ? <LoadingSpinner /> : <Component {...props} />; } };
而後是加上typesweb
interface WithLoadingProps { loading: boolean; } const withLoading = <P extends object>(Component: React.ComponentType<P>) => class WithLoading extends React.Component<P & WithLoadingProps> { render() { const { loading, ...props } = this.props; return loading ? <LoadingSpinner /> : <Component {...props as P} />; } };
這裏發生了一些事情,因此咱們將把它分解:typescript
interface WithLoadingProps { loading: boolean; }
在這裏,聲明一個props的interface,將會被添加到被包裹的組件上。redux
<P extends object>(Component: React.ComponentType<P>)
這裏咱們使用泛型:P表示傳遞到HOC的組件的props。React.ComponentType<P>
是 React.FunctionComponent<P> | React.ClassComponent<P>
的別名,表示傳遞到HOC的組件能夠是類組件或者是函數組件。數組
class WithLoading extends React.Component<P & WithLoadingProps>
在這裏,咱們定義從HOC返回的組件,並指定該組件將包括傳入組件的props(P)和HOC的props(WithLoadingProps)。它們經過 & 組合在一塊兒。app
const { loading, ...props } = this.props;
最後,咱們使用loading props有條件地顯示加loading圖或傳遞了本身props的組件:less
return loading ? <LoadingSpinner /> : <Component {...props as P} />;
注意:因爲typescript中可能存在的bug,所以從typescript v3.2開始,這裏須要類型轉換(props as p)。
咱們的withloading HOC也能夠重寫以返回函數組件而不是類:
const withLoading = <P extends object>( Component: React.ComponentType<P> ): React.FC<P & WithLoadingProps> => ({ loading, ...props }: WithLoadingProps) => loading ? <LoadingSpinner /> : <Component {...props as P} />;
這裏,咱們對對象rest/spread
也有一樣的問題,所以經過設置顯式的返回類型React.FC<P & WithLoadingProps>
來解決這個問題,但只能在無狀態功能組件中使用WithLoadingProps。
注意:React.FC是React.FunctionComponent的縮寫。在早期版本的@types/react中,是React.SFC或React.StatelessFunctionalComponent。
injectors是更常見的HOC形式,但更難爲其設置類型。除了向組件中注入props外,在大多數狀況下,當包裹好後,它們也會移除注入的props,這樣它們就不能再從外部設置了。react redux的connect就是是injector HOC的一個例子,可是在本文中,咱們將使用一個更簡單的例子,它注入一個計數器值並回調以增長和減小該值:
import { Subtract } from 'utility-types'; export interface InjectedCounterProps { value: number; onIncrement(): void; onDecrement(): void; } interface MakeCounterState { value: number; } const makeCounter = <P extends InjectedCounterProps>( Component: React.ComponentType<P> ) => class MakeCounter extends React.Component< Subtract<P, InjectedCounterProps>, MakeCounterState > { state: MakeCounterState = { value: 0, }; increment = () => { this.setState(prevState => ({ value: prevState.value + 1, })); }; decrement = () => { this.setState(prevState => ({ value: prevState.value - 1, })); }; render() { return ( <Component {...this.props as P} value={this.state.value} onIncrement={this.increment} onDecrement={this.decrement} /> ); } };
這裏有幾個關鍵區別:
export interface InjectedCounterProps { value: number; onIncrement(): void; onDecrement(): void; }
咱們給將要注入到組件的props聲明一個interface,該接口將被導出,以便這些props可由被HOC包裹的組件使用:
import makeCounter, { InjectedCounterProps } from './makeCounter'; interface CounterProps extends InjectedCounterProps { style?: React.CSSProperties; } const Counter = (props: CounterProps) => ( <div style={props.style}> <button onClick={props.onDecrement}> - </button> {props.value} <button onClick={props.onIncrement}> + </button> </div> ); export default makeCounter(Counter);
<P extends InjectedCounterProps>(Component: React.ComponentType<P>)
咱們再次使用泛型,可是此次,你要確保傳入到HOC的組件包含注入到其中的props,不然,你將收到一個編譯錯誤。
class MakeCounter extends React.Component< Subtract<P, InjectedCounterProps>, MakeCounterState >
HOC返回的組件使用Piotrek Witek’s的utility-types包中的subtract,它將從傳入組件的props中減去注入的props,這意味着若是它們設置在生成的包裹組件上,則會收到編譯錯誤:

結合這兩種模式,咱們將在計數器示例的基礎上,容許將最小和最大計數器值傳遞給HOC,而HOC又被它截取並使用,而不將它們傳遞給組件:
export interface InjectedCounterProps { value: number; onIncrement(): void; onDecrement(): void; } interface MakeCounterProps { minValue?: number; maxValue?: number; } interface MakeCounterState { value: number; } const makeCounter = <P extends InjectedCounterProps>( Component: React.ComponentType<P> ) => class MakeCounter extends React.Component< Subtract<P, InjectedCounterProps> & MakeCounterProps, MakeCounterState > { state: MakeCounterState = { value: 0, }; increment = () => { this.setState(prevState => ({ value: prevState.value === this.props.maxValue ? prevState.value : prevState.value + 1, })); }; decrement = () => { this.setState(prevState => ({ value: prevState.value === this.props.minValue ? prevState.value : prevState.value - 1, })); }; render() { const { minValue, maxValue, ...props } = this.props; return ( <Component {...props as P} value={this.state.value} onIncrement={this.increment} onDecrement={this.decrement} /> ); } };
這裏,Subtract與types交集相結合,將組件自身的props與HOCs自身的props相結合,減去注入組件的props:
Subtract<P, InjectedCounterProps> & MakeCounterProps
除此以外,與其餘兩種模式相比,沒有真正的差別須要強調,可是這個示例確實帶來了一些高階組件的問題。這些並非真正特定於typescript的,但值得詳細說明,以便咱們能夠討論如何使用typescript來解決這些問題。
首先,MinValue和MaxValue被HOC攔截,而不是傳遞給組件。可是,你也許但願它們是這樣的,這樣你就能夠基於這些值禁用遞增/遞減按鈕,或者向用戶顯示一條消息。若是用HOC,你也能夠簡單地修改它來注入這些值,可是若是你沒有(例如,它來自一個NPM包),這就將會是一個問題。
其次,由HOC注入的prop有一個很是通用的名稱;若是要將其用於其餘目的,或者若是要從多個HOC注入prop,則此名稱可能與其餘注入的prop衝突。您能夠將名稱更改成不太通用的解決方案,但就解決方案而言,這不是一個很好的解決方案!