https://blog.csdn.net/sinat_17775997/article/details/84203095 大佬文章,留着備用。
寫在最前面
- 為了在 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>
中對state
的readonly
標識。
函數式組件的聲明
-
// SFC: stateless function components
-
// v16.7起,由於hooks的加入,函數式組件也可以使用state,所以這個命名不准確。新的react聲明文件里,也定義了React.FC類型^_^
-
const List: React.SFC<IProps> = props => null
-
復制代碼
class組件都要指明props和state類型嗎?
是的
。只要在組件內部使用了props
和state
,就需要在聲明組件時指明其類型。- 但是,你可能發現了,只要我們初始化了
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中的高階組件本質上是個高階函數的調用,所以高階組件的使用,我們既可以使用函數式方法調用,也可以使用裝飾器。但是在TS中,編譯器會對裝飾器作用的值做簽名一致性檢查,而我們在高階組件中一般都會返回新的組件,並且對被作用的組件的
props
進行修改(添加、刪除)等。這些會導致簽名一致性校驗失敗,TS
會給出錯誤提示。這帶來兩個問題:
第一,是否還能使用裝飾器語法調用高階組件?
- 這個答案也得分情況:如果這個高階組件正確聲明了其函數簽名,那么應該使用函數式調用,比如
withRouter
:
-
import { RouteComponentProps } from 'react-router-dom';
-
-
const App = withRouter( class extends Component<RouteComponentProps> {
-
// ...
-
});
-
-
// 以下調用是ok的
-
<App />
-
復制代碼
如上的例子,我們在聲明組件時,注解了組件的props是路由的
RouteComponentProps
結構類型,但是我們在調用App組件時,並不需要給其傳遞RouteComponentProps
里說具有的location
、history
等值,這是因為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都做出類型聲明。