優雅的在 react 中使用 TypeScript

Pandaaa發表於2018-11-15

寫在最前面

  • 為了在 react 中更好的使用 ts,進行一下討論
  • 怎麼合理的再 react 中使用 ts 的一些特性讓程式碼更加健壯

討論幾個問題,react 元件的宣告?react 高階元件的宣告和使用?class元件中 props 和 state 的使用?...

在 react 中使用 ts 的幾點原則和變化

  • 所有用到jsx語法的檔案都需要以tsx字尾命名
  • 使用元件宣告時的Component<P, S>泛型引數宣告,來代替PropTypes!
  • 全域性變數或者自定義的window物件屬性,統一在專案根下的global.d.ts中進行宣告定義
  • 對於專案中常用到的介面資料物件,在types/目錄下定義好其結構化型別宣告

宣告React元件

  • react中的元件從定義方式上來說,分為類元件和函式式元件。

  • 類元件的宣告

class App extends Component<IProps, IState> {
    static defaultProps = {
        // ...
    }
    
    readonly state = {
        // ...
    }; 
    // 小技巧:如果state很複雜不想一個個都初始化,可以結合型別斷言初始化state為空物件或者只包含少數必須的值的物件:  readonly state = {} as IState;
}
複製程式碼

需要特別強調的是,如果用到了state,除了在宣告元件時通過泛型引數傳遞其state結構,還需要在初始化state時宣告為 readonly

這是因為我們使用 class properties 語法對state做初始化時,會覆蓋掉Component<P, S>中對statereadonly標識。

函式式元件的宣告

// SFC: stateless function components
// v16.7起,由於hooks的加入,函式式元件也可以使用state,所以這個命名不準確。新的react宣告檔案裡,也定義了React.FC型別^_^
const List: React.SFC<IProps> = props => null
複製程式碼

class元件都要指明props和state型別嗎?

  • 是的。只要在元件內部使用了propsstate,就需要在宣告元件時指明其型別。
  • 但是,你可能發現了,只要我們初始化了state,貌似即使沒有宣告state的型別,也可以正常呼叫以及setState。沒錯,實際情況確實是這樣的,但是這樣子做其實是讓元件丟失了對state的訪問和型別檢查!
// bad one
class App extends Component {
    state = {
        a: 1,
        b: 2
    }
 
    componentDidMount() {
        this.state.a // ok: 1
 
        // 假如通過setState設定並不存在的c,TS無法檢查到。
        this.setState({
            c: 3
        });
        
        this.setState(true); // ???
    }
    // ...
}
 
// React Component
class Component<P, S> {
        constructor(props: Readonly<P>);
        setState<K extends keyof S>(
            state: ((prevState: Readonly<S>, props: Readonly<P>) => (Pick<S, K> | S | null)) | (Pick<S, K> | S | null),
            callback?: () => void
        ): void;
        forceUpdate(callBack?: () => void): void;
        render(): ReactNode;
        readonly props: Readonly<{ children?: ReactNode }> & Readonly<P>;
        state: Readonly<S>;
        context: any;
        refs: {
            [key: string]: ReactInstance
        };
    }
 
 
// interface IState{
//    a: number,
//    b: number
// }

// good one
class App extends Component<{}, { a: number, b: number }> {
   
    readonly state = {
        a: 1,
        b: 2
    }
    
    //readonly state = {} as IState,斷言全部為一個值
 
    componentDidMount() {
        this.state.a // ok: 1
 
        //正確的使用了 ts 泛型指示了 state 以後就會有正確的提示
        // error: '{ c: number }' is not assignable to parameter of type '{ a: number, b: number }'
        this.setState({
            c: 3
        });
    }
    // ...
}
複製程式碼

使用react高階元件

什麼是 react 高階元件?裝飾器?

  • 因為react中的高階元件本質上是個高階函式的呼叫,所以高階元件的使用,我們既可以使用函式式方法呼叫,也可以使用裝飾器。但是在TS中,編譯器會對裝飾器作用的值做簽名一致性檢查,而我們在高階元件中一般都會返回新的元件,並且對被作用的元件的props進行修改(新增、刪除)等。這些會導致簽名一致性校驗失敗,TS會給出錯誤提示。這帶來兩個問題:

第一,是否還能使用裝飾器語法呼叫高階元件?

  • 這個答案也得分情況:如果這個高階元件正確宣告瞭其函式簽名,那麼應該使用函式式呼叫,比如 withRouter
import { RouteComponentProps } from 'react-router-dom';
 
const App = withRouter(class extends Component<RouteComponentProps> {
    // ...
});
 
// 以下呼叫是ok的
<App />
複製程式碼

如上的例子,我們在宣告元件時,註解了元件的props是路由的RouteComponentProps結構型別,但是我們在呼叫App元件時,並不需要給其傳遞RouteComponentProps裡說具有的locationhistory等值,這是因為withRouter這個函式自身對齊做了正確的型別宣告。

第二,使用裝飾器語法或者沒有函式型別簽名的高階元件怎麼辦?


如何正確的宣告高階元件?

  • 就是將高階元件注入的屬性都宣告可選(通過Partial這個對映型別),或者將其宣告到額外的injected元件例項屬性上。 我們先看一個常見的元件宣告:
import { RouteComponentProps } from 'react-router-dom';
 
// 方法一
@withRouter
class App extends Component<Partial<RouteComponentProps>> {
    public componentDidMount() {
        // 這裡就需要使用非空型別斷言了
        this.props.history!.push('/');
    }
    // ...
});
 
// 方法二
@withRouter
class App extends Component<{}> {
    get injected() {
        return this.props as RouteComponentProps
    }
 
    public componentDidMount() {
        this.injected.history.push('/');
    }
    // ...
複製程式碼

如何正確的宣告高階元件?

interface IUserCardProps {
    name: string;
    avatar: string;
    bio: string;
 
    isAdmin?: boolean;
}
class UserCard extends Component<IUserCardProps> { /* ... */}
複製程式碼

上面的元件要求了三個必傳屬性引數:name、avatar、bio,isAdmin是可選的。加入此時我們想要宣告一個高階元件,用來給UserCard傳遞一個額外的布林值屬性visible,我們也需要在UserCard中使用這個值,那麼我們就需要在其props的型別裡新增這個值:

interface IUserCardProps {
    name: string;
    avatar: string;
    bio: string;
    visible: boolean;
 
    isAdmin?: boolean;
}
@withVisible
class UserCard extends Component<IUserCardProps> {
    render() {
        // 因為我們用到visible了,所以必須在IUserCardProps裡宣告出該屬性
        return <div className={this.props.visible ? '' : 'none'}>...</div>
    }
}
 
function withVisiable(WrappedComponent) {
    return class extends Component {
        render() {
            return <WrappedComponent {..this.props}  visiable={true} />
        }
    }
}
複製程式碼
  • 但是這樣一來,我們在呼叫UserCard時就會出現問題,因為visible這個屬性被標記為了必需,所以TS會給出錯誤。這個屬性是由高階元件注入的,所以我們肯定是不能要求都再傳一下的。

可能你此時想到了,把visible宣告為可選。沒錯,這個確實就解決了呼叫元件時visible必傳的問題。這確實是個解決問題的辦法。但是就像上一個問題裡提到的,這種應對辦法應該是對付哪些沒有型別宣告或者宣告不正確的高階元件的。

所以這個就要求我們能正確的宣告高階元件:

interface IVisible {
    visible: boolean;
}
 
 //排除 IVisible
function withVisible<Self>(WrappedComponent: React.ComponentType<Self & IVisible>): React.ComponentType<Omit<Self, 'visible'>> {
    return class extends Component<Self> {
        render() {
            return <WrappedComponent {...this.props}  visible={true} />
        }
    }
}
複製程式碼

如上,我們宣告withVisible這個高階元件時,利用泛型和型別推導,我們對高階元件返回的新的元件以及接收的引數元件的props都做出型別宣告。

參考:

  • 組內大佬的wiki

相關文章