三千字讲清TypeScript与React的实战技巧

很多时候虽然我们了解了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中会报错:

三千字讲清TypeScript与React的实战技巧

原因就是我们没有定义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都会有代码提示:

三千字讲清TypeScript与React的实战技巧

如果我们这个组件是业务中的通用组件的话,甚至可以加上注释:

interface IProps {
 /**
 * logo的地址
 */
 logo?: string
 className?: string
 alt?: string
}
</pre>

这样在其他同事调用此组件的时候,除了代码提示外甚至会有注释的说明:

三千字讲清TypeScript与React的实战技巧

有状态组件

现在假设我们开始编写一个Todo应用:

三千字讲清TypeScript与React的实战技巧

首先需要编写一个todoInput组件:

三千字讲清TypeScript与React的实战技巧

如果我们按照JavaScript的写法,只要写一个开头就会碰到一堆报错

三千字讲清TypeScript与React的实战技巧

有状态组件除了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。

如下:

三千字讲清TypeScript与React的实战技巧

接下来我们需要添加组件方法,大多数情况下这个方法是本组件的私有方法,这个时候需要加入访问控制符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>

那么这么多类型的定义,我们怎么记得住呢?遇到其它没见过的事件,难道要去各种搜索才能定义类型吗?其实这里有一个小技巧,当我们在组件中输入事件对应的名称时,会有相关的定义提示,我们只要用这个提示中的类型就可以了。

三千字讲清TypeScript与React的实战技巧

默认属性

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作为类型传入组件,同时实例化类,作为默认属性。

三千字讲清TypeScript与React的实战技巧

用class作为props类型以及生产默认属性实例有以下好处:

  • 代码量少:一次编写,既可以作为类型也可以实例化作为值使用
  • 避免错误:分开编写一旦有一方造成书写错误不易察觉

这种方法虽然不错,但是之后我们会发现问题了,虽然我们已经声明了默认属性,但是在使用的时候,依然显示inputSetting可能未定义。

三千字讲清TypeScript与React的实战技巧

在这种情况下有一种最快速的解决办法,就是加!,它的作用就是告诉编译器这里不是undefined,从而避免报错。

三千字讲清TypeScript与React的实战技巧

如果你觉得这个方法过于粗暴,那么可以选择三目运算符做一个简单的判断:

三千字讲清TypeScript与React的实战技巧

如果你还觉得这个方法有点繁琐,因为如果这种情况过多,我们需要额外写非常多的条件判断,而更重要的是,我们明明已经声明了值,就不应该再做条件判断了,应该有一种方法让编译器自己推导出这里的类型不是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>

我们看到依旧会报错:

三千字讲清TypeScript与React的实战技巧

其实这个时候我们需要一个函数,将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绝大部分问题了.

大家可以点个关注哦~转发评论,留下自己的脚步!之后还会给大家带来一系列技术干货,包括最新面试题也会不断和大家分享的哈!
Java学习、面试;文档、视频资源点击免费获取

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 217,907评论 6 506
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,987评论 3 395
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 164,298评论 0 354
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,586评论 1 293
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,633评论 6 392
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,488评论 1 302
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,275评论 3 418
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,176评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,619评论 1 314
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,819评论 3 336
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,932评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,655评论 5 346
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,265评论 3 329
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,871评论 0 22
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,994评论 1 269
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,095评论 3 370
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,884评论 2 354

推荐阅读更多精彩内容

  • 作为一个合格的开发者,不要只满足于编写了可以运行的代码。而要了解代码背后的工作原理;不要只满足于自己的程序...
    六个周阅读 8,448评论 1 33
  • 3. JSX JSX是对JavaScript语言的一个扩展语法, 用于生产React“元素”,建议在描述UI的时候...
    pixels阅读 2,824评论 0 24
  • 以下内容是我在学习和研究React时,对React的特性、重点和注意事项的提取、精练和总结,可以做为React特性...
    科研者阅读 8,232评论 2 21
  • 40、React 什么是React?React 是一个用于构建用户界面的框架(采用的是MVC模式):集中处理VIE...
    萌妹撒阅读 1,016评论 0 1
  • 原教程内容详见精益 React 学习指南,这只是我在学习过程中的一些阅读笔记,个人觉得该教程讲解深入浅出,比目前大...
    leonaxiong阅读 2,834评论 1 18