【笔记】React的ts类型检查与注意点
Sep 6, 2020笔记reacttypescriptReact的ts类型检查与注意点
配置
首先进行编译配置。配置很简单,先安装@types/react
包(npm i -D @types/react
),然后在tsconfig.json
的编译配置项compilerOptions
中配置"jsx"
项为"react"
即可。
*jsx项的选项
"preserve"
:模式下对代码的编译会保留 jsx 格式,并输出一份.jsx
后缀的文件;"react"
:模式下对代码的编译是直接转换成 React.createElement,并输出一份.js
后缀的文件;"react-native"
:模式下对代码的编译会保留 jsx 格式,并输出一份.js
后缀的文件。
Mode | Input | Output | Output File Extension |
---|---|---|---|
preserve | <div /> |
<div /> |
.jsx |
react | <div /> |
React.createElement("div") |
.js |
react-native | <div /> |
<div /> |
.js |
常用检查
函数组件的类型检查
使用FC(Function Component)类型来声明函数组件
FC定义了默认的 props(如 children)以及一些静态属性(如 defaultProps)。如
1 | import React, { FC } from 'react'; |
也可以直接使用普通函数来进行组件声明,这种形式更加灵活:
1 | interface DemoComponentProps { |
与之类似的还有SFC,但现在已经不再建议使用了,具体原因可见下文
@types/react
源码分析。
定义 Props 类型
通常使用 ComponentName|Props
的格式命名 Props 类型。如:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17import React from 'react'
interface DemoComponentProps {
title: string;
content: string;
}
function DemoComponent (props: DemoComponentProps) {
const {
title,
content,
} = props;
return (<>
<p>title: {title}</p>
<p>content: {content}</p>
</>)
}
Children
我们通过设置 React.ReactNode 来设置children。如
1 | import React from 'react' |
默认 props 声明
TypeScript 3.0 开始已经支持了 defaultProps ,这也意味着我们可以很方便的定义默认值。如:
1 | import React, { PropsWithChildren } from 'react'; |
这种方式很简洁, 只不过 defaultProps 的类型和组件本身的 props 没有关联性, 这会使得 defaultProps 无法得到类型约束, 所以必要时进一步显式声明 defaultProps 的类型:
1 | Hello.defaultProps = { name: 'Wayne' } as Partial<HelloProps>; |
Dispatch接口
Dispatch<any>
泛型接口,用于定义dispatch的类型,常用于useReducer生成的dispatch中。
1 | /** |
泛型函数组件
泛型函数组件在列表型或容器型的组件中比较常用, 直接使用FC无法满足需求:
1 | import React from 'react'; |
子组件声明
使用Parent.Child形式的 JSX 可以让节点父子关系更加直观, 它类似于一种命名空间的机制,可以避免命名冲突,ant design中就大量使用了这类形式。相比ParentChild这种命名方式, Parent.Child更为优雅些。如:
1 | import React, { PropsWithChildren } from 'react'; |
Forwarding Refs
React.forwardRef
在 16.3 新增, 可以用于转发 ref,适用于 HOC 和函数组件。其暴露方法可以使用 ComponentName|Methods
的命名规则
1 | /* MyModal.tsx */ |
1 | /* Test.tsx */ |
类组件的类型检查
继承 Component<P, S={}>
或 PureComponent<P, S={}>
泛型类,接收两个参数:
P
:props的类型定义S
:state的类型定义
1 | import React from 'react' |
使用static defaultProps定义默认 props
在 defaultProps 中定义的 props 可以不需要?
可选操作符修饰,demo如上一个。
子组件声明
类组件可以使用静态属性形式声明子组件,如:
1 | import React from 'react' |
泛型组件
与函数组件的泛型组件类似,如:
1 | import React from 'react' |
其他函数组件的类型定义
HTML元素的扩展属性
你可以通过 JSX.IntrinsicElements
集合确保你能够设置一个元素的所有HTML属性。如:
1 | import React from 'react' |
预设属性:1
2
3
4
5
6
7
8
9
10import React from 'react'
type ButtonProps =
Omit<JSX.IntrinsicElements["button"], "type">;
function Button({ ...allProps }: ButtonProps) {
return <button type="button" {...allProps} />;
}
const z = <Button type="button">Hi</Button>;
Context
Context 提供了一种跨组件间状态共享机制。通常我们使用 Name|ContextValue
的命名规范声明Context的类型。
1 | import React, { FC, useContext } from 'react'; |
*高阶组件的类型检查
老实说不建议使用HOC。高阶组件笨重且难以理解,容易造成嵌套地狱(wrapper),对 Typescript 类型化也不友好。不过还是举个栗子:
1 | import React, { FC } from 'react'; |
或1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23/**
* 抽取出通用的高阶组件类型
*/
type HOC<InjectedProps, OwnProps = {}> = <P>(
Component: React.ComponentType<P & InjectedProps>,
) => React.ComponentType<P & OwnProps>;
/**
* 声明注入的Props
*/
export interface ThemeProps {
primary: string;
secondary: string;
}
export const withTheme: HOC<ThemeProps> = Component => props => {
// 假设theme从context中获取
const fakeTheme: ThemeProps = {
primary: 'red',
secondary: 'blue',
};
return <Component {...fakeTheme} {...props} />;
};
*Render Props
React 的 props(包括 children)并没有限定类型,它可以是一个函数。于是就有了 render props, 这是和高阶组件一样常见的模式:
1 | import React from 'react'; |
事件的类型定义
使用handleEvent命名事件处理器.
如果存在多个相同事件处理器, 则按照 handle|Type|Event
的格式命名, 例如: handleNameChange
。
1 | import React from 'react'; |
事件Events
常用 Event 事件对象类型:
ClipboardEvent<T = Element>
剪贴板事件对象DragEvent<T = Element>
拖拽事件对象ChangeEvent<T = Element>
Change 事件对象KeyboardEvent<T = Element>
键盘事件对象MouseEvent<T = Element>
鼠标事件对象TouchEvent<T = Element>
触摸事件对象WheelEvent<T = Element>
滚轮事件对象AnimationEvent<T = Element>
动画事件对象TransitionEvent<T = Element>
过渡事件对象FormEvent
:一个react的form表单event的类型
demos:
1 | <form |
1 | <input |
内置事件处理器的类型
@types/react
内置了以下事件处理器的类型:
1 | type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void }['bivarianceHack']; |
可以简洁地声明事件处理器类型:
1 | import { ChangeEventHandler } from 'react'; |
自定义组件暴露事件处理器类型
和原生 html 元素一样, 自定义组件应该暴露自己的事件处理器类型, 尤其是较为复杂的事件处理器, 这样可以避免开发者手动为每个事件处理器的参数声明类型
自定义事件处理器类型以 ComponentName|Event|Handler
的格式命名。 为了和原生事件处理器类型区分, 不使用EventHandler形式的后缀
1 | import React, { FC, useState } from 'react'; |
其他类型检查
获取原生元素 props 定义
有些场景我们希望原生元素扩展一下一些 props. 所有原生元素 props 都继承了React.HTMLAttributes, 某些特殊元素也会扩展了自己的属性, 例如InputHTMLAttributes. 具体可以参考React.createElement方法的实现
1 | import React, { FC } from 'react'; |
styled-components
styled-components 是流行的CSS-in-js库, Typescript 在 2.9 支持泛型标签模板,因此可以简单地对 styled-components 创建的组件进行类型约束
1 | // 依赖于@types/styled-components |
为没有提供 Typescript 声明文件的第三方库自定义模块声明
1 | // global.d.ts |
axios的类型定义
1 | import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse,AxiosError } from 'axios' |
Preact的类型检查
首先tsconfig.json配置与React不同:1
2
3
4
5
6
7
8
9{
"compilerOptions": {
"jsx": "preserve",
"jsxFactory": "h", // Preact的虚拟DOM解析
"jsxFragmentFactory": "Fragment" // Preact的fragment组件解析
...
}
...
}
并且比较“坑”的是,需要在每个组件中引入Fragment和h,用以告知ts的解析模式。1
import { Fragment, h } from 'preact'
@types/react
源码分析
@types/react
的源码其实就是仔细定义了一些React涉及到的接口,源码的备注也比较完整。源码地址:github @types/react
SFC(Stateless Function Component)和FC
早前一直好奇SFC和FC的区别,在看源码时发现1
2
3
4
5
6
7
8
9
10type SFC<P = {}> = FunctionComponent<P>;
type FC<P = {}> = FunctionComponent<P>;
interface FunctionComponent<P = {}> {
(props: PropsWithChildren<P>, context?: any): ReactElement | null;
propTypes?: WeakValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
在目前的@types/react
定义中,SFC和FC指向都是FunctionComponent
这个接口,也就是说它们是一样的。
主要原因可见DefinitelyTyped pull-30364,简单来说,SFC是被弃用的,但为保证兼容旧业务和提升语义性仍然保留。总的结果就是React.SFC、React.StatelessComponent、React.FC、React.FunctionComponent都是同一个接口
*以前的SFC
1 | type SFC<P = {}> = StatelessComponent<P>; |
发现区别其实就是propTypes项ValidationMap
和WeakValidationMap
的区别(type PropsWithChildren<P> = P & { children?: ReactNode };
,ValidationMap的定义可见@types/prop-types
),即旧SFC的props校验更为严格:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17// ValidationMap
export type ValidationMap<T> = { [K in keyof T]?: Validator<T[K]> };
// WeakValidationMap
type WeakValidationMap<T> = {
[K in keyof T]?: null extends T[K]
? Validator<T[K] | null | undefined>
: undefined extends T[K]
? Validator<T[K] | null | undefined>
: Validator<T[K]>
};
// Validator
export interface Validator<T> {
(props: object, propName: string, componentName: string, location: string, propFullName: string): Error | null;
[nominalTypeHack]?: T;
}
Events
@types/react
花了很大的篇幅进行了事件接口的封装,毕竟DOM是前端中最复杂的模块之一了。如触摸事件的封装:
1 | interface TouchEvent<T = Element> extends SyntheticEvent<T, NativeTouchEvent> { |
@types/react
中的Content接口
1 | interface Context<T> { |
可以发现我们需要传递一个类型,从而使得里面的参数类型也是一致。
其他
@types/react
有许多设计比较巧妙的地方,可以通过《@types react 值得注意的 TS 技巧》进行引读。
注意点
1.不要直接使用export default导出匿名函数组件
如1
2
3export default (props: {}) => {
return <div>hello react</div>;
};
这种方式导出的组件在React Inspector查看时会显示为Unknown。可修改为:1
2
3export default Hello (props: {}) => {
return <div>hello react</div>;
};
2.放弃PropTypes
有了 Typescript 之后可以安全地约束 Props 和 State, 没有必要引入 React.PropTypes, 而且它的表达能力比较弱
3.关于是否使用FC的争议
关于是否使用FC一直存在争议,如《typescript-react-why-i-dont-use-react-fc》,其中给了5条理由,总得来说就是不用FC会更加灵活和更具拓展性。
以我个人的观点来看,FC会让代码更具语义性,如果能保证项目没有迁移类React技术栈(preact、taro、rax等)的情况下,建议使用。
相关链接
- https://fettblog.eu/typescript-react-component-patterns/
- https://www.typescriptlang.org/docs/handbook/jsx.html
- https://stackoverflow.com/questions/53885993/react-16-7-react-sfc-is-now-deprecated/53886046#53886046
- https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react
- https://github.com/DefinitelyTyped/DefinitelyTyped/tree/73176410af2dfd18dd7c4a28e19fb182a42a239e
- https://fettblog.eu/typescript-react-why-i-dont-use-react-fc/
- https://github.com/dt-fe/weekly/blob/v2/147.%20%E7%B2%BE%E8%AF%BB%E3%80%8A%40types%20react%20%E5%80%BC%E5%BE%97%E6%B3%A8%E6%84%8F%E7%9A%84%20TS%20%E6%8A%80%E5%B7%A7%E3%80%8B.md
- https://fettblog.eu/typescript-vite-preact/
Author
My name is Micheal Wayne and this is my blog.
I am a front-end software engineer.
Contact: michealwayne@163.com