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

本文首發於微信公衆號「程序員面試官」前端

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

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

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

快速啓動TypeScript版react

使用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: ''
        }
    }
}

複製代碼

細心的人會問,這個時候需不須要給PropsState加上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組件,其實此組件也是一個受控組件,當咱們改變inputvalue的時候須要調用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絕大部分問題了.


相關文章
相關標籤/搜索