很多時候雖然我們瞭解了TypeScript相關的基礎知識,但是這不足以保證我們在實際專案中可以靈活運用,比如現在絕大部分前端開發者的專案都是依賴於框架的,因此我們需要來講一下React與TypeScript應該如何結合運用。
如果你僅僅瞭解了一下TypeScript的基礎知識就上手框架會碰到非常多的坑(比如筆者自己),如果你是React開發者一定要看過本文之後再進行實踐。
快速啟動TypeScript版react
使用TypeScript編寫react程式碼,除了需要typescript這個庫之外,還至少需要額外的兩個庫:
yarn add -D @types/{react,react-dom}
</pre>
可能有人好奇@types開頭的這種庫是什麼?
由於非常多的JavaScript庫並沒有提供自己關於TypeScript的宣告檔案,導致TypeScript的使用者無法享受這種庫帶來的型別,因此社群中就出現了一個專案DefinitelyTyped,他定義了目前市面上絕大多數的JavaScript庫的宣告,當人們下載JavaScript庫相關的@types宣告時,就可以享受此庫相關的型別定義了。
當然,為了方便我們選擇直接用TypeScript官方提供的react啟動模板。
create-react-app react-ts-app --scripts-version=react-scripts-ts
</pre>
無狀態元件
我們用初始化好了上述模板之後就需要進行正式編寫程式碼了。
無狀態元件是一種非常常見的react元件,主要用於展示UI,初始的模板中就有一個logo圖,我們就可以把它封裝成一個Logo元件。
在JavaScript中我們往往是這樣封裝元件的:
import * as React from 'react' export const Logo = props => { const { logo, className, alt } = props return ( <img src={logo} className={className} alt={alt} /> ) } </pre>
但是在TypeScript中會報錯:
原因就是我們沒有定義props的型別,我們用interface定義一下props的型別,那麼是不是這樣就行了:
import * as React from 'react' interface IProps { logo?: string className?: string alt?: string } export const Logo = (props: IProps) => { const { logo, className, alt } = props return ( <img src={logo} className={className} alt={alt} /> ) } </pre>
這樣做在這個例子中看似沒問題,但是當我們要用到children的時候是不是又要去定於children型別?
比如這樣:
interface IProps { logo?: string className?: string alt?: string children?: ReactNode } </pre>
其實有一種更規範更簡單的辦法,type SFC<P>其中已經定義了children型別。
我們只需要這樣使用:
export const Logo: React.SFC<IProps> = props => { const { logo, className, alt } = props return ( <img src={logo} className={className} alt={alt} /> ) } </pre>
我們現在就可以替換App.tsx中的logo元件,可以看到相關的props都會有程式碼提示:
如果我們這個元件是業務中的通用元件的話,甚至可以加上註釋:
interface IProps { /** * logo的地址 */ logo?: string className?: string alt?: string } </pre>
這樣在其他同事呼叫此元件的時候,除了程式碼提示外甚至會有註釋的說明:
有狀態元件
現在假設我們開始編寫一個Todo應用:
首先需要編寫一個todoInput元件:
如果我們按照JavaScript的寫法,只要寫一個開頭就會碰到一堆報錯
有狀態元件除了props之外還需要state,對於class寫法的元件要泛型的支援,即Component<P, S>,因此需要傳入傳入state和props的型別,這樣我們就可以正常使用props和state了。
import * as React from 'react' interface Props { handleSubmit: (value: string) => void } interface State { itemText: string } export class TodoInput extends React.Component<Props, State> { constructor(props: Props) { super(props) this.state = { itemText: '' } } } </pre>
細心的人會問,這個時候需不需要給Props和State加上Readonly,因為我們的資料都是不可變的,這樣會不會更嚴謹?
其實是不用的,因為React的宣告檔案已經自動幫我們包裝過上述型別了,已經標記為readonly。
如下:
接下來我們需要新增元件方法,大多數情況下這個方法是本元件的私有方法,這個時候需要加入訪問控制符private。
private updateValue(value: string) { this.setState({ itemText: value }) } </pre>
接下來也是大家經常會碰到的一個不太好處理的型別,如果我們想取某個元件的ref,那麼應該如何操作?
比如我們需要在元件更新完畢之後,使得input元件focus。
首先,我們需要用React.createRef建立一個ref,然後在對應的元件上引入即可。
private inputRef = React.createRef<HTMLInputElement>() ... <input ref={this.inputRef} className="edit" value={this.state.itemText} /> </pre>
需要注意的是,在createRef這裡需要一個泛型,這個泛型就是需要ref元件的型別,因為這個是input元件,所以型別是HTMLInputElement,當然如果是div元件的話那麼這個型別就是HTMLDivElement。
受控元件
再接著講TodoInput元件,其實此元件也是一個受控元件,當我們改變input的value的時候需要呼叫this.setState來不斷更新狀態,這個時候就會用到『事件』型別。
由於React內部的事件其實都是合成事件,也就是說都是經過React處理過的,所以並不原生事件,因此通常情況下我們這個時候需要定義React中的事件型別。
對於input元件onChange中的事件,我們一般是這樣宣告的:
private updateValue(e: React.ChangeEvent<HTMLInputElement>) { this.setState({ itemText: e.target.value }) } </pre>
當我們需要提交表單的時候,需要這樣定義事件型別:
private handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() if (!this.state.itemText.trim()) { return } this.props.handleSubmit(this.state.itemText) this.setState({itemText: ''}) } </pre>
那麼這麼多型別的定義,我們怎麼記得住呢?遇到其它沒見過的事件,難道要去各種搜尋才能定義型別嗎?其實這裡有一個小技巧,當我們在元件中輸入事件對應的名稱時,會有相關的定義提示,我們只要用這個提示中的型別就可以了。
預設屬性
React中有時候會運用很多預設屬性,尤其是在我們編寫通用元件的時候,之前我們介紹過一個關於預設屬性的小技巧,就是利用class來同時宣告型別和建立初始值。
再回到我們這個專案中,假設我們需要通過props來給input元件傳遞屬性,而且需要初始值,我們這個時候完全可以通過class來進行程式碼簡化。
// props.type.ts interface InputSetting { placeholder?: string maxlength?: number } export class TodoInputProps { public handleSubmit: (value: string) => void public inputSetting?: InputSetting = { maxlength: 20, placeholder: '請輸入todo', } } </pre>
再回到TodoInput元件中,我們直接用class作為型別傳入元件,同時例項化類,作為預設屬性。
用class作為props型別以及生產預設屬性例項有以下好處:
- 程式碼量少:一次編寫,既可以作為型別也可以例項化作為值使用
- 避免錯誤:分開編寫一旦有一方造成書寫錯誤不易察覺
這種方法雖然不錯,但是之後我們會發現問題了,雖然我們已經宣告瞭預設屬性,但是在使用的時候,依然顯示inputSetting可能未定義。
在這種情況下有一種最快速的解決辦法,就是加!,它的作用就是告訴編譯器這裡不是undefined,從而避免報錯。
如果你覺得這個方法過於粗暴,那麼可以選擇三目運算子做一個簡單的判斷:
利用高階型別解決預設屬性報錯
我們現在需要先宣告defaultProps的值:
const todoInputDefaultProps = { inputSetting: { maxlength: 20, placeholder: '請輸入todo', } } </pre>
接著定義元件的props型別
type Props = { handleSubmit: (value: string) => void children: React.ReactNode } & Partial<typeof todoInputDefaultProps> </pre>
Partial的作用就是將型別的屬性全部變成可選的,也就是下面這種情況:
{ inputSetting?: { maxlength: number; placeholder: string; } | undefined; } </pre>
那麼現在我們使用Props是不是就沒有問題了?
export class TodoInput extends React.Component<Props, State> { public static defaultProps = todoInputDefaultProps ... public render() { const { itemText } = this.state const { updateValue, handleSubmit } = this const { inputSetting } = this.props return ( <form onSubmit={handleSubmit} > <input maxLength={inputSetting.maxlength} type='text' value={itemText} onChange={updateValue} /> <button type='submit' >新增todo</button> </form> ) } ... } </pre>
我們看到依舊會報錯:
其實這個時候我們需要一個函式,將defaultProps中已經宣告值的屬性從『可選型別』轉化為『非可選型別』。
我們先看這麼一個函式:
const createPropsGetter = <DP extends object>(defaultProps: DP) => { return <P extends Partial<DP>>(props: P) => { type PropsExcludingDefaults = Omit<P, keyof DP> type RecomposedProps = DP & PropsExcludingDefaults return (props as any) as RecomposedProps } } </pre>
這個函式接受一個defaultProps物件,<DP extends object>這裡是泛型約束,代表DP這個泛型是個物件,然後返回一個匿名函式。
再看這個匿名函式,此函式也有一個泛型P,這個泛型P也被約束過,即<P extends Partial<DP>>,意思就是這個泛型必須包含可選的DP型別(實際上這個泛型P就是元件傳入的Props型別)。
接著我們看型別別名PropsExcludingDefaults,看這個名字你也能猜出來,它的作用其實是剔除Props型別中關於defaultProps的部分,很多人可能不清楚Omit這個高階型別的用法,其實就是一個語法糖:
ype Omit<P, keyof DP> = Pick<P, Exclude<keyof P, keyof DP>> </pre>
而型別別名RecomposedProps則是將預設屬性的型別DP與剔除了預設屬性的Props型別結合在一起。
其實這個函式只做了一件事,把可選的defaultProps的型別剔除後,加入必選的defaultProps的型別,從而形成一個新的Props型別,這個Props型別中的defaultProps相關屬性就變成了必選的。
這個函式可能對於初學者理解上有一定難度,涉及到TypeScript文件中的高階型別,這算是一次綜合應用。
完整程式碼如下:
import * as React from 'react' interface State { itemText: string } type Props = { handleSubmit: (value: string) => void children: React.ReactNode } & Partial<typeof todoInputDefaultProps> const todoInputDefaultProps = { inputSetting: { maxlength: 20, placeholder: '請輸入todo', } } export const createPropsGetter = <DP extends object>(defaultProps: DP) => { return <P extends Partial<DP>>(props: P) => { type PropsExcludingDefaults = Omit<P, keyof DP> type RecomposedProps = DP & PropsExcludingDefaults return (props as any) as RecomposedProps } } const getProps = createPropsGetter(todoInputDefaultProps) export class TodoInput extends React.Component<Props, State> { public static defaultProps = todoInputDefaultProps constructor(props: Props) { super(props) this.state = { itemText: '' } } public render() { const { itemText } = this.state const { updateValue, handleSubmit } = this const { inputSetting } = getProps(this.props) return ( <form onSubmit={handleSubmit} > <input maxLength={inputSetting.maxlength} type='text' value={itemText} onChange={updateValue} /> <button type='submit' >新增todo</button> </form> ) } private updateValue(e: React.ChangeEvent<HTMLInputElement>) { this.setState({ itemText: e.target.value }) } private handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() if (!this.state.itemText.trim()) { return } this.props.handleSubmit(this.state.itemText) this.setState({itemText: ''}) } } </pre>
高階元件
關於在TypeScript如何使用HOC一直是一個難點,我們在這裡就介紹一種比較常規的方法。
我們繼續來看TodoInput這個元件,其中我們一直在用inputSetting來自定義input的屬性,現在我們需要用一個HOC來包裝TodoInput,其作用就是用高階元件向TodoInput注入props。
我們的高階函式如下:
import * as hoistNonReactStatics from 'hoist-non-react-statics' import * as React from 'react' type InjectedProps = Partial<typeof hocProps> const hocProps = { inputSetting: { maxlength: 30, placeholder: '請輸入待辦事項', } } export const withTodoInput = <P extends InjectedProps>( UnwrappedComponent: React.ComponentType<P>, ) => { type Props = Omit<P, keyof InjectedProps> class WithToggleable extends React.Component<Props> { public static readonly UnwrappedComponent = UnwrappedComponent public render() { return ( <UnwrappedComponent inputSetting={hocProps} {...this.props as P} /> ); } } return hoistNonReactStatics(WithToggleable, UnwrappedComponent) } </pre>
如果你搞懂了上一小節的內容,這裡應該沒有什麼難度。
這裡我們的P表示傳遞到HOC的元件的props,React.ComponentType<P> 是React.FunctionComponent<P> | React.ClassComponent<P>的別名,表示傳遞到HOC的元件可以是類元件或者是函式元件。
其餘的地方Omit as P等都是講過的內容,讀者可以自行理解,我們不再像上一小節那樣一行行解釋了。
只需要這樣使用:
const HOC = withTodoInput<Props>(TodoInput) </pre>
小結
我們總結了最常見的幾種元件在TypeScript下的編寫方式,通過這篇文章你可以解決在React使用TypeScript絕大部分問題了.
大家可以點個關注哦~評論,留下自己的腳步!之後還會給大家帶來一系列技術乾貨,包括最新面試題也會不斷和大家分享,大家可以去我的主頁【點選進入】免費領取哈!