如何解決Typescript對React props的類型檢查

最近在學mobx,感覺挺靈活高效的,沒有Redux那麼繁瑣的約定,對於一些小項目而言無疑是比較好的選擇。同時我的react項目都是基於Typescript開發的,所以嘗試用Typescrip來寫Mobx。總體而言,體驗還是不錯的,但是在使用Provider/inject時,遇到了一些問題:

import * as React from react;nimport { inject, observer, Provider } from mobx-react;nimport { observable } from mobx;nimport { ChangeEvent } from react;nnninterface ContextType {n color: string;n}nn@inject(color)nclass Message extends React.Component<ContextType> {n render() {n return (n <div>{this.props.color}</div>n );n }n}nnclass MessageWrap extends React.Component {n render() {n return (n <div>n <Message/>n </div>n );n }n}nn@observernexport default class Seven extends React.Component {n n @observablen color: string = red;nn changeColor = (event: ChangeEvent<HTMLInputElement>) => {n this.color = String(event.target.value) as string;n }nn render() {n return (n <Provider color={this.color}>n <div>n <MessageWrap/>n <input onChange={this.changeColor}/>n </div>n </Provider>n );n }nn}n

代碼就是上面這段,這裡遇到的問題是:Provider基於Context API;但在其嵌套的子組件(Message)使用inject裝飾器之後,需要訪問props來獲取從Provider傳遞下來的observable值,而這個時候Typescript會對React的props進行嚴格的類型檢查。所以只能通過層層傳遞props來通過Typescript的類型檢查,這個時候Context的跨組件傳遞特性也就沒了。這個時候想了一想,不得已只能使用可選屬性來規避這個問題了,就像這樣:

interface ContextType {n color?: string;n}nn@inject(color)nclass Message extends React.Component<ContextType> {n render() {n return (n <div>{this.props.color}</div>n );n }n}n

通過可選屬性,規避掉了Typescript的對Props的類型檢查,但這個時候就有潛在的問題了,如果這個時候Provider沒有傳遞color這個observable,也是能通過檢查的,所以需要對進行傳遞過來的observable值進行額外的判斷了。在Google上搜了搜相關的內容,發現大家對這個問題都挺困擾的。然後發現了這篇文章: medium.com/@prashaantt/

在Typescript2.0之前,空類型是能賦值給其他類型的,就像這樣:

let s: string;ns = "some string";ns = null;ns = undefined;n

而在Typescript2.0開啟了strictNullChecks 嚴格的空檢查之後,就會規避掉上面這些問題,就像下面這樣:

let s1: string;ns1 = "some string";ns1 = undefined; // Type undefined is not assignable to type stringns1 = null; // Type null is not assignable to type stringnnlet s2: string | undefined;ns2 = "another string";ns2 = undefined;ns2 = null; // Type null is not assignable to type string | undefinedn

但是開啟了這個選項後,就會對interface中的可選屬性產生影響。還是上面的那個問題,前面也說了需要進行額外的判斷,如果不進行判斷,就像下面這樣:

interface ContextType {n color?: string;n}nn@inject(color)nclass Message extends React.Component<ContextType> {n render() {n return (n <div>{this.props.color.toUpperCase()}</div>n );n }n}n

這個時候編譯器會拋出錯誤:

同時,不止如此,在真實的應用場景中,可能會有更多的可選屬性,而且這些屬性有可能來自第三方類庫的高階組件,類似於react-router和mobx,這個時候就跟我遇到的問題非常的像了:

import { UserStore } from "../../stores/UserStore";nninterface MyComponentProps {n name: string;n countryCode?: string;n userStore?: UserStore;n router?: InjectedRouter;n}nn@inject("userStore")n@withRoutern@observernclass MyComponent extends React.Component<MyComponentProps, {}> {n render() {n const { name, countryCode, userStore, router } = this.props;n n return (n <div>n User: { name } <br />n Name: { userStore && userStore.getUserName(name) } <br />n Country: { countryCode && countryCode.toUpperCase() } <br />n <buttonn onClick={() => { router && router.push(`/users/${name}`) }}n >n Check out usern </button>n </div>n );n }n}n

在調用上面的組件時,雖然高階組件會自動將對應的屬性注入到組件樹中,這也就保證了這些Props是一定存在的,但是Typescript仍然強制開發者去做更多的判斷,非常疲憊。

如果你不想去做更多的判斷,就不能使用可選屬性,像下面這樣去定義props的interface:

interface MyComponentProps {n name: string;n countryCode?: string;n userStore: UserStore; // made requiredn router: InjectedRouter; // made requiredn}n

但在嵌套使用該組件的時候,你就會非常手足無措了:

class OtherComponent extends React.Component<{}, {}> {n render() {n return (n <MyComponentn name="foo"n countryCode="in"n // Error: router and userStore are missing!n />n );n }n}n

有沒有更優雅的解決方案呢?當然是有的。我們可以利用介面的繼承,具體的解決方案如下:

interface MyComponentProps {n name: string;n countryCode?: string;n}nninterface InjectedProps extends MyComponentProps {n userStore: UserStore;n router: InjectedRouter;n}nn@inject("userStore")n@withRoutern@observernclass MyComponent extends React.Component<MyComponentProps, {}> {n get injected() {n return this.props as InjectedProps;n }nn render() {n const { name, countryCode } = this.props;n const { userStore, router } = this.injected;n ...n }n}n

我們將可選屬性抽離出來,單獨定義成一個介面,然後該介面繼承非可選屬性的介面。在定義組件的時候只需要傳入非可選屬性的介面,然後在調用props時,利用斷言將該非可選屬性的介面強製成可選屬性的介面,這樣就規避掉了Typescript對props的額外判斷,非常優雅。

推薦閱讀:

Mobx 思想的實現原理,及與 Redux 對比
如何評價數據流管理框架 MobX ?
高性能 MobX 模式(part 2)- 響應變化

TAG:React | TypeScript | MobX |