優雅的在 react 中使用 TypeScript

寫在最前面

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

討論幾個問題,react 組件的聲明?react 高階組件的聲明和使用?class組件中 props 和 state 的使用?...javascript

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

  • 全部用到jsx語法的文件都須要以tsx後綴命名
  • 使用組件聲明時的Component<P, S>泛型參數聲明,來代替PropTypes!
  • 全局變量或者自定義的window對象屬性,統一在項目根下的global.d.ts中進行聲明定義
  • 對於項目中經常使用到的接口數據對象,在types/目錄下定義好其結構化類型聲明

聲明React組件

  • react中的組件從定義方式上來講,分爲類組件和函數式組件。html

  • 類組件的聲明java

class App extends Component<IProps, IState> {
    static defaultProps = {
        // ...
    }
    
    readonly state = {
        // ...
    }; 
    // 小技巧:若是state很複雜不想一個個都初始化,能夠結合類型斷言初始化state爲空對象或者只包含少數必須的值的對象: readonly state = {} as IState;
}
複製代碼

須要特別強調的是,若是用到了state,除了在聲明組件時經過泛型參數傳遞其state結構,還須要在初始化state時聲明爲 readonlyreact

這是由於咱們使用 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-router

  • 由於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這個函數自身對齊作了正確的類型聲明。app

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


如何正確的聲明高階組件?

  • 就是將高階組件注入的屬性都聲明可選(經過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的類型裏添加這個值:less

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必傳的問題。這確實是個解決問題的辦法。可是就像上一個問題裏提到的,這種應對辦法應該是對付哪些沒有類型聲明或者聲明不正確的高階組件的。dom

因此這個就要求咱們能正確的聲明高階組件:函數

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
相關文章
相關標籤/搜索