三千字講清TypeScript與React的實戰技巧

不少時候雖然咱們瞭解了TypeScript相關的基礎知識,可是這不足以保證咱們在實際項目中能夠靈活運用,好比如今絕大部分前端開發者的項目都是依賴於框架的,所以咱們須要來說一下React與TypeScript應該如何結合運用。前端

若是你僅僅瞭解了一下TypeScript的基礎知識就上手框架會碰到很是多的坑(好比筆者本身),若是你是React開發者必定要看過本文以後再進行實踐。react

快速啓動TypeScript版react

使用TypeScript編寫react代碼,除了須要typescript這個庫以外,還至少須要額外的兩個庫:面試

yarn add -D @types/{react,react-dom}
</pre>

可能有人好奇@types開頭的這種庫是什麼?typescript

因爲很是多的JavaScript庫並無提供本身關於TypeScript的聲明文件,致使TypeScript的使用者沒法享受這種庫帶來的類型,所以社區中就出現了一個項目DefinitelyTyped,他定義了目前市面上絕大多數的JavaScript庫的聲明,當人們下載JavaScript庫相關的@types聲明時,就能夠享受此庫相關的類型定義了。數組

固然,爲了方便咱們選擇直接用TypeScript官方提供的react啓動模板。app

create-react-app react-ts-app --scripts-version=react-scripts-ts
</pre>

無狀態組件

咱們用初始化好了上述模板以後就須要進行正式編寫代碼了。框架

無狀態組件是一種很是常見的react組件,主要用於展現UI,初始的模板中就有一個logo圖,咱們就能夠把它封裝成一個Logo組件。dom

在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中會報錯:this

緣由就是咱們沒有定義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,從而避免報錯。

若是你以爲這個方法過於粗暴,那麼能夠選擇三目運算符作一個簡單的判斷:

 
若是你還以爲這個方法有點繁瑣,由於若是這種狀況過多,咱們須要額外寫很是多的條件判斷,而更重要的是,咱們明明已經聲明瞭值,就不該該再作條件判斷了,應該有一種方法讓編譯器本身推導出這裏的類型不是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絕大部分問題了.

你們能夠點個關注哦~評論,留下本身的腳步!以後還會給你們帶來一系列技術乾貨,包括最新面試題也會不斷和你們分享,你們能夠去個人主頁【點擊進入】免費領取哈!

相關文章
相關標籤/搜索