本文首發於微信公衆號「程序員面試官」前端
不少時候雖然咱們瞭解了TypeScript相關的基礎知識,可是這不足以保證咱們在實際項目中能夠靈活運用,好比如今絕大部分前端開發者的項目都是依賴於框架的,所以咱們須要來說一下React與TypeScript應該如何結合運用。react
若是你僅僅瞭解了一下TypeScript的基礎知識就上手框架會碰到很是多的坑(好比筆者本身),若是你是React開發者必定要看過本文以後再進行實踐。git
使用TypeScript編寫react代碼,除了須要typescript
這個庫以外,還至少須要額外的兩個庫:程序員
yarn add -D @types/{react,react-dom}
複製代碼
可能有人好奇@types
開頭的這種庫是什麼?github
因爲很是多的JavaScript庫並無提供本身關於TypeScript的聲明文件,致使TypeScript的使用者沒法享受這種庫帶來的類型,所以社區中就出現了一個項目DefinitelyTyped,他定義了目前市面上絕大多數的JavaScript庫的聲明,當人們下載JavaScript庫相關的@types
聲明時,就能夠享受此庫相關的類型定義了。面試
固然,爲了方便咱們選擇直接用TypeScript官方提供的react啓動模板。typescript
create-react-app react-ts-app --scripts-version=react-scripts-ts
複製代碼
咱們用初始化好了上述模板以後就須要進行正式編寫代碼了。數組
無狀態組件是一種很是常見的react組件,主要用於展現UI,初始的模板中就有一個logo圖,咱們就能夠把它封裝成一個Logo
組件。bash
在JavaScript中咱們每每是這樣封裝組件的:微信
import * as React from 'react' export const Logo = props => { const { logo, className, alt } = props return ( <img src={logo} className={className} alt={alt} /> ) } 複製代碼
可是在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} /> ) } 複製代碼
這樣作在這個例子中看似沒問題,可是當咱們要用到children
的時候是否是又要去定於children
類型?
好比這樣:
interface IProps {
logo?: string
className?: string
alt?: string
children?: ReactNode
}
複製代碼
其實有一種更規範更簡單的辦法,type SFC<P>
其中已經定義了children
類型。
咱們只須要這樣使用:
export const Logo: React.SFC<IProps> = props => { const { logo, className, alt } = props return ( <img src={logo} className={className} alt={alt} /> ) } 複製代碼
咱們如今就能夠替換App.tsx
中的logo
組件,能夠看到相關的props都會有代碼提示:
若是咱們這個組件是業務中的通用組件的話,甚至能夠加上註釋:
interface IProps {
/**
* logo的地址
*/
logo?: string
className?: string
alt?: string
}
複製代碼
這樣在其餘同事調用此組件的時候,除了代碼提示外甚至會有註釋的說明:
如今假設咱們開始編寫一個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: '' } } } 複製代碼
細心的人會問,這個時候需不須要給Props
和State
加上Readonly
,由於咱們的數據都是不可變的,這樣會不會更嚴謹?
實際上是不用的,由於React的聲明文件已經自動幫咱們包裝過上述類型了,已經標記爲readonly
。
以下:
接下來咱們須要添加組件方法,大多數狀況下這個方法是本組件的私有方法,這個時候須要加入訪問控制符private
。
private updateValue(value: string) {
this.setState({ itemText: value })
}
複製代碼
接下來也是你們常常會碰到的一個不太好處理的類型,若是咱們想取某個組件的ref
,那麼應該如何操做?
好比咱們須要在組件更新完畢以後,使得input
組件focus
。
首先,咱們須要用React.createRef
建立一個ref,而後在對應的組件上引入便可。
private inputRef = React.createRef<HTMLInputElement>() ... <input ref={this.inputRef} className="edit" value={this.state.itemText} /> 複製代碼
須要注意的是,在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 })
}
複製代碼
當咱們須要提交表單的時候,須要這樣定義事件類型:
private handleSubmit(e: React.FormEvent<HTMLFormElement>) { e.preventDefault() if (!this.state.itemText.trim()) { return } this.props.handleSubmit(this.state.itemText) this.setState({itemText: ''}) } 複製代碼
那麼這麼多類型的定義,咱們怎麼記得住呢?遇到其它沒見過的事件,難道要去各類搜索才能定義類型嗎?其實這裏有一個小技巧,當咱們在組件中輸入事件對應的名稱時,會有相關的定義提示,咱們只要用這個提示中的類型就能夠了。
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', } } 複製代碼
再回到TodoInput
組件中,咱們直接用class做爲類型傳入組件,同時實例化類,做爲默認屬性。
用class做爲props類型以及生產默認屬性實例有如下好處:
這種方法雖然不錯,可是以後咱們會發現問題了,雖然咱們已經聲明瞭默認屬性,可是在使用的時候,依然顯示inputSetting
可能未定義。
在這種狀況下有一種最快速的解決辦法,就是加!
,它的做用就是告訴編譯器這裏不是undefined,從而避免報錯。
若是你以爲這個方法過於粗暴,那麼能夠選擇三目運算符作一個簡單的判斷:
若是你還以爲這個方法有點繁瑣,由於若是這種狀況過多,咱們須要額外寫很是多的條件判斷,而更重要的是,咱們明明已經聲明瞭值,就不該該再作條件判斷了,應該有一種方法讓編譯器本身推導出這裏的類型不是undefined,這就涉及到一些高級類型了。
咱們如今須要先聲明defaultProps的值:
const todoInputDefaultProps = { inputSetting: { maxlength: 20, placeholder: '請輸入todo', } } 複製代碼
接着定義組件的props類型
type Props = { handleSubmit: (value: string) => void children: React.ReactNode } & Partial<typeof todoInputDefaultProps> 複製代碼
Partial
的做用就是將類型的屬性所有變成可選的,也就是下面這種狀況:
{
inputSetting?: {
maxlength: number;
placeholder: string;
} | undefined;
}
複製代碼
那麼如今咱們使用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> ) } ... } 複製代碼
咱們看到依舊會報錯:
其實這個時候咱們須要一個函數,將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 } } 複製代碼
這個函數接受一個defaultProps
對象,<DP extends object>
這裏是泛型約束,表明DP
這個泛型是個對象,而後返回一個匿名函數。
再看這個匿名函數,此函數也有一個泛型P
,這個泛型P
也被約束過,即<P extends Partial<DP>>
,意思就是這個泛型必須包含可選的DP
類型(實際上這個泛型P就是組件傳入的Props類型)。
接着咱們看類型別名PropsExcludingDefaults
,看這個名字你也能猜出來,它的做用實際上是剔除Props
類型中關於defaultProps
的部分,不少人可能不清楚Omit
這個高級類型的用法,其實就是一個語法糖:
type Omit<P, keyof DP> = Pick<P, Exclude<keyof P, keyof DP>> 複製代碼
而類型別名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: ''}) } } 複製代碼
關於在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) } 複製代碼
若是你搞懂了上一小節的內容,這裏應該沒有什麼難度。
這裏咱們的P表示傳遞到HOC的組件的props,React.ComponentType<P>
是 React.FunctionComponent<P> | React.ClassComponent<P>
的別名,表示傳遞到HOC的組件能夠是類組件或者是函數組件。
其他的地方Omit
as P
等都是講過的內容,讀者能夠自行理解,咱們再也不像上一小節那樣一行行解釋了。
只須要這樣使用:
const HOC = withTodoInput<Props>(TodoInput)
複製代碼
咱們總結了最多見的幾種組件在TypeScript下的編寫方式,經過這篇文章你能夠解決在React使用TypeScript絕大部分問題了.