三千字讲清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绝大部分问题了.


相关文章
相关标签/搜索