TypeScript 如何完美地書寫 React 中的 HOC?
這裡的HOC是指react的Higher-Order Components
假設我要給每個組件注入一個顏色值。
我們有了這麼一個高階組件(HOC)
const { Consumer: GetColor } = React.createContext("#000000");
const InjectColor = (Component) =&> class extends React.Component {
public render() {
return &{color =&> & ;}&
}
};
現在就可以用這個HOC給任意組件注入color了
class NeedColor extends React.Component&<{ color: string }, {}&> {
public render() {
return &;
}
}
const Block = InjectColor(NeedColor);
但是上面這些代碼活生生把ts寫成了js,接下來我要開始寫ts了。
首先看不慣InjectColor的參數和返回值類型居然是any。所以我給InjectColor加上類型約束
const InjectColor = (Component: React.ComponentType&<{ color: string }&>): React.ComponentClass&<{}&> =&>
class extends React.Component& {
public render() {
return &{color =&> & ;}&
}
};
但是很明顯這麼寫局限性太大了,如果我的NeedColor不只是需要color
class NeedColor extends React.Component&<{ color: string, width: number }, {}&> {
public render() {
return &;
}
}
所以還需要給InjectColor加上一個泛型(如果是用go寫的就不需要這個泛型了)
type Exclude&
= T extends U ? never : T;
type Omit&= Pick& &>;
const InjectColor =
&(Component: React.ComponentType& ): React.ComponentClass& &> =&>
class extends React.Component&, any&> {
public render() {
return &{color =&> & ;}&
}
};
這裡的Omit是把color屬性從Props中去掉的意思。
現在看起來一切都是那麼完美。。。。。
但是我們可不止想注入color,我們還有store,router等等亂七八糟的要注入。
難道每個都這樣寫類型嗎?太不程序員了。
我們發現這些HOC有一個共同的特徵,
他們接受一個組件,返回一個被注入了一些props的組件。
所以可以這麼定義HOC的類型
type HOC&
= & (Component: React.ComponentType& ) =&> React.ComponentType& &>;
然後改寫我們的InjectColor
//這是用class
const InjectColor: HOC&<{ color: string }&> = Component =&> class extends React.Component& {
public render() {
return &{color =&> & ;}&
}
} as React.ComponentClass&;
//這裡as一下是因為class的類型不會根據返回值類型來做推斷而是直接固定為一個匿名類型,也可以直接as any
//下面是function的寫法,會根據返回值類型做推斷,因為我們的InjectColor沒用到有狀態組件的功能所以也能這麼寫。
const InjectColor: HOC&<{ color: string }&> = Component =&> props =&>
&{color =&> & ;}&
然後現在整個文件變成了這樣
type Exclude&
= T extends U ? never : T;
type Omit&= Pick& &>;
type HOC&= & (Component: React.ComponentType& ) =&> React.ComponentType& &>; const { Consumer: GetColor } = React.createContext("#000000");
const InjectColor: HOC&<{ color: string }&> = Component =&> props =&>
&{color =&> & ;}& class NeedColor extends React.Component&<{ color: string, width: number }, {}&> {
public render() {
return &;
}
}const Block = InjectColor(NeedColor);
render(&
, document.getElementById("root"));
這還不夠,在實際項目中我們不止一個HOC
const InjectHeight: HOC&<{ height: number }&> = "細節不實現了" as any;
const InjectColor: HOC&<{ color: string }&> = Component =&> props =&>
&{color =&> & ;}& class NeedColor extends React.Component&<{ color: string, width: number, height: number,//新增height屬性 }, {}&> {
public render() {
return &;
}
}const Block = InjectHeight(InjectColor(NeedColor));
甚至還有connect這種
const connect = &
(mapState: (state: any) =&> State): HOC& =&> "細節不實現了" as any; class NeedColor extends React.Component&<{ color: string, width: number, height: number, text: string,//新增text屬性 }, {}&> {
public render() {
const { text, color, width, height } = this.props;
return &{text}&;
}
}const Block = connect(state =&> ({ text: state.text }))(InjectHeight(InjectColor(NeedColor)));
這樣組合函數,丑就一個字。。。。特別是參數更多,組合層數更多的時候。更丑
畢竟學過一丁點函數式編程,這時候自然而然就得想起compose函數啊
噹噹噹噹~~~~~
function compose&
(hoc1: HOC& , hoc2: HOC& ): HOC& ;
function compose&(hoc1: HOC& , hoc2: HOC& , hoc3: HOC& ): HOC& ;
function compose&(hoc1: HOC& , hoc2: HOC& , hoc3: HOC& , hoc4: HOC& ): HOC& ;
function compose(...hocs: Array&) {
return (c: React.ComponentType) =&> hocs.reduce((acc, hoc) =&> hoc(acc), c);
}
別笑,雖然傻,但是能用,用上去看看效果
const Block = compose(
connect(state =&> ({ text: state.text })),
InjectHeight,
InjectColor)(NeedColor);
妙不可言。但是compose的實現好蠢啊。
放著oop的鏈式調用不使,非得用函數,吃飽了撐的。
改造一下
class Lifter&
{
public static lift = &(hoc: HOC& ): Lifter& =&> new Lifter([hoc]);
private constructor(private hocs: Array&&>) { }
public lift = &(hoc: HOC& ): Lifter& =&> new Lifter([...this.hocs, hoc]);
public use: HOC&= c =&> this.hocs.reduce((acc: any, hoc) =&> hoc(acc), c);
}
const Block = Lifter
.lift(connect(state =&> ({ text: state.text })))
.lift(InjectHeight)
.lift(InjectColor)
.use(NeedColor);
看起來還不錯。再看一下整體效果
type Exclude&
= T extends U ? never : T;
type Omit&= Pick& &>;
type HOC&= & (Component: React.ComponentType& ) =&> React.ComponentType& &>; class Lifter&
{
public static lift = &(hoc: HOC& ): Lifter& =&> new Lifter([hoc]);
private constructor(private hocs: Array&&>) { }
public lift = &(hoc: HOC& ): Lifter& =&> new Lifter([...this.hocs, hoc]);
public use: HOC&= c =&> this.hocs.reduce((acc: any, hoc) =&> hoc(acc), c);
}const { Consumer: GetColor } = React.createContext("#000000");
const InjectHeight: HOC&<{ height: number }&> = "細節不實現了" as any;
const connect = &
(mapState: (state: any) =&> State): HOC& =&> "細節不實現了" as any; const InjectColor: HOC&<{ color: string }&> = Component =&> props =&>
&{color =&> & ;}& class NeedColor extends React.Component&<{ color: string, width: number, height: number, text: string, }, {}&> {
public render() {
const { text, color, width, height } = this.props;
return &{text}&;
}
}const Block = Lifter
.lift(connect(state =&> ({ text: state.text })))
.lift(InjectHeight)
.lift(InjectColor)
.use(NeedColor);render(&
, document.getElementById("root"));
我們發現,NeedColor根本不需要用class寫組件,因為NeedColor是無狀態的。
再改一下
const NeedColor: React.SFC&<{ color: string, width: number, height: number, text: string, }&> = ({ text, color, height, width }) =&> &
{text}&;
還是感覺有點啰嗦,這個組件既然只有一個地方用,幹嘛不直接寫到use里去,還省了寫類型。
但是width並不是注入的,所以我們這裡要對HOC類型做一些改變
type OldHOC&
= & (Component: React.ComponentType& ) =&> React.ComponentType& &>; type HOC&
= & (Component: React.ComponentType& ) =&> React.ComponentType& ;
之前那個是官方給的HOC類型,下面那個是我喜歡用的HOC類型。
官方給的使用了Omit的類型缺陷在於你沒辦法單獨指定非注入的Props類型。
因為Props是繼承於InjectProps的,你指定的Props必須包含所有InjectProps的屬性。
也就是說使用OldHOC我需要添加一個非注入的width需要這麼寫:
const Block = Lifter
.lift(connect(state =&> ({ text: state.text })))
.lift(InjectHeight)
.lift(InjectColor)
.use&<{ width: number text: string,//一大堆不必要的 color: string,//一大堆不必要的 height: number,//一大堆不必要的 }&>(({ text, color, height, width }) =&> &{text}&);
而使用第二個HOC只需要這麼寫:
const Block = Lifter
.lift(connect(state =&> ({ text: state.text })))
.lift(InjectHeight)
.lift(InjectColor)
.use&<{ width: number }&>(({ text, color, height, width }) =&> &{text}&);
但是很明顯,官方不用我寫的這個是有原因的。
我用的這個的缺陷在於,ts的類型推導沒辦法在知道InjectProps和InjectPropsProps這兩個泛型的具體類型的情況下從PropsInjectProps中分離Props的類型。。。。。。
這個缺陷表現出來的現象是這樣的:
const C: React.SFC&<{ height: number, color: string }&> = "細節不實現了" as any;
const X = InjectColor(C);//X的類型為React.ComponentType&<{ height: number, color: string }&>
ts並沒有因為InjectProps有color,PropsInjectProps有color和height推斷出Props有height。
而是推斷出Props有height和color。。。。。
解決方法是永遠手動標非注入的屬性
const X = InjectColor&<{ height: number }&>(({ height, color }) =&> null);
兩種HOC類型寫法都有利有弊,喜歡用哪種就用哪種吧。
因為第一種對我繼續完善HOC的DSL來說弊端更多,所以我選擇第二種。
由於新的HOC類型,寫無狀態組件更加方便了,但是我要是想要有狀態怎麼辦呢?
相信HOC的力量,recompose庫了解一下,withState了解一下:
const withState =
&
(setName: SetName, name: Name, value: Value): HOC&<{ [K in keyof SetName]: (v: Value) =&> void } { [K in keyof Name]: Value }&> =&> "細節不實現了" as any;
使用效果:
const Block = Lifter
.lift(connect(state =&> ({ text: state.text })))
.lift(InjectHeight)
.lift(InjectColor)
.lift(withState("setCount", "count", 0))//注入了一個state
.use&<{ width: number }&>(({ text, color, height, width, setCount, count }) =&> &setCount(count + 1)}&>{text} clicked {count} times&);
光有state怎麼行,有狀態組件還得有生命周期啊。
先說到這,洗澡睡覺下次再說。
整個文件現在是這樣的:
type Exclude&
= T extends U ? never : T;
type Omit&= Pick& &>; type HOC&
= & (Component: React.ComponentType& ) =&> React.ComponentType& ; class Lifter&
{
public static lift = &(hoc: HOC& ): Lifter& =&> new Lifter([hoc]);
private constructor(private hocs: Array&&>) { }
public lift = &(hoc: HOC& ): Lifter& =&> new Lifter([...this.hocs, hoc]);
public use: HOC&= c =&> this.hocs.reduce((acc: any, hoc) =&> hoc(acc), c);
}const { Consumer: GetColor } = React.createContext("#000000");
const InjectHeight: HOC&<{ height: number }&> = "細節不實現了" as any;
const withState =
&
(setName: SetName, name: Name, value: Value): HOC&<{ [K in SetName]: (v: Value) =&> void } { [K in Name]: Value }&> =&> "細節不實現了" as any;const connect = &
(mapState: (state: any) =&> State): HOC& =&> "細節不實現了" as any; const InjectColor: HOC&<{ color: string }&> = Component =&> props =&>
&{color =&> & ;}& const Block = Lifter
.lift(connect(state =&> ({ text: state.text })))
.lift(InjectHeight)
.lift(InjectColor)
.lift(withState("setCount", "count", 0))
.use&<{ width: number }&>(({ text, color, height, width, setCount, count }) =&> &setCount(count + 1)}&>{text} clicked {count} times&);render(&
, document.getElementById("root"));
出浴後的故事
請參考這篇文章:
https://levelup.gitconnected.com/ultimate-react-component-patterns-with-typescript-2-8-82990c516935?levelup.gitconnected.com不清楚有沒有完美的方法。個人最常用的是PropsOf,快速獲取被包裹組件Props的一個類型函數。用法,這行獲取了div的所有Props。
以及這裡的一些類型函數。基本可以把類型寫得比較簡潔。
還有用好自帶的React.forwardRef(),以及遵守官網上的其他最佳實踐。
自己項目里的一段,僅供參考。有幾個any是因為我不想折騰了,搞半天沒搞好直接any。。
雖然用any不太好,主要是我這還用redux包了一下,官網也沒有很好的解決方案不用any,但是不用的話太折騰浪費時間啊。該用any的時候還是用any的,別愣頭青,就是不用any才算完美?匿了。可以參考別人已經寫過的 declaration...
https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/recompose/index.d.ts
然後自己也有寫過一點點
https://github.com/xialvjun/create-react-context/blob/v2/src/Context.ts
推薦閱讀:※有趣也有用的現代類型系統
※angular 和 typescript 到底是否適合最佳實踐?
※求教:TypeScript override 父類的get set 報錯?
※你所不知道的 Typescript 與 Redux 類型優化
※有哪些公司在使用或者準備使用Angular2?TAG:TypeScript | React |