前言
当你刚开始用 Typescript 写业务时,你写几行代码,就多一处错误提示。为了消除这些错误提示,你查遍TS文档和谷歌搜索,最后可能还解决不了,写代码的成本陡然上升啊。不禁感叹,这真的值得吗?
值得。相信听过 Typescript 的都知道它的好:类型系统提供了严格的类型检查,让我们编写的应用更加健壮;提供了JS还尚不支持的新特性;配合 VSCode 有友好的代码提示。更关键的是在你不断的实践中,你慢慢会形成一种类型思维,做一个有类型思维的开发者。我们说语言、框架都是工具,思维才是核心竞争力啊。
下面的内容,我会重点介绍用 Typescript 开发 React 的实践。希望对你在使用 Typescript 上有帮助。
React 组件
平时使用 React 写组件时,最常用的方式有类组件和函数式组件两种,看看我们如何使用 TS 来编写:
1. Function Components
a. 函数声明式
对于声明式的 React 函数组件,我们一般会对 Props 进行类型声明,使用接口 interface 来定义 Props 类型。
b. 函数表达式
对于表达式方式,我们还是使用接口 interface 来给 Props 进行类型定义,写法上用一对「尖括号」。并且我们将 Person 函数组件定义为 FunctionComponent,FunctionComponent 是属于 React 内置的接口,它自动会给 Props 定义可选属性 children,而不需要在 IPerson 接口里单独定义,其中 children 是一个 ReactNode。
2. Class Component
使用 TS 写类组件时,继承的 React.Component 是一个泛型(React.ComponentReact.Component<P = {}, S = {}, SS = any>
,第三个泛型参数是 SnapShot,默认是 any 类型。
React 是单向数据流,Props 是不允许在子组件内被修改的,那我们需要手动为每个属性都加上 readonly?
不需要,React.Component<P,S>
,已经将它们标记为 immutable(不可变的)。
Hooks
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。相信很多前端已经把 React Hooks 作为日常开发的首选了。那么结合 Typescript,该如何使用呢?
1. useState
我们知道 Typescript 是有类型推断的,对于在使用 useState 时,类型推断多数时候都是好用的。
如果想给变量 null 初始值,我们可以使用联合类型:
2. useReducer
useReducer 中我可以使用到 Typescript 的高级用法,可识别联合类型(Discriminated Unions)。我们用可识别联合类型来定义 action。最后别忘了给 reducer 定义返回类型。
3. useRef
使用 useRef 时,我们一般有两种方式去创建没有初始值的 ref 容器。
// option 1
const ref1 = useRef<HTMLInputElement>(null);
// option 2
const ref2 = useRef<HTMLInputElement>(null!);
使用第二种方式会绕过对于 ref2.current 的 nullchecks 检查,而第一种方式则不会。
null!
是一个非空的断言运算符。断言在它之前的任何表达式都不为null或者undefined。这意味着,当你正在使用为null的ref实例,Typescript也不会把它当作null,进而导致编译失败。
使用第一种方式创建的 ref 实例是一个 RefObject
,而第二种方式则是一个 MutableRefObject
,如果你想为 ref1.current 赋值会出现类型错误,因为它是 Read Only(只读的)。
如果你想自己管理“实例变量”,那也可以这样改造 option1:
// option 3
const ref3 = useRef<HTMLInputElement | null>(null);
这时,ref3 实例就是 MutableRefObject
。当编写代码时,我们需要自行检查 ref3 和 ref3.current 是否存在,一旦存在则 ref3 就是 HTMLInputElement
类型,具有 focus
方法。
4. Custom Hooks
当我们在写自定义 Hook 时,Hook 返回一个数组,则要避免类型推断,因为 Typescript 会默认推断为一个联合类型,而实际情况你可能希望是为数组每个位置定义不同的类型,如下图是一个 Loading 的自定义 Hook:
我们将返回的数组断言 const 时,Typescript 就会把数组每个位置指定不同的类型了。
当然,你也可以自己定义函数返回类型:
事件处理
Typescript 中 @types/react
提供了各种事件的类型,比如下面的 form 表单为例:
1. Forms and Event
如上图,你可以为 input 的 onChange
事件和 form 的 onSubmit
事件分别定义类型,所有的类型定义都有相同的格式:React.事件名<ReactNode>
,其中事件名和 ReactNode 都可以从@types/react/global.d.ts
中查到。并且,因为 @type
定义让 IDE 给我们很多键入提示。
🎈如果你不在乎事件类型,你也可以使用 React.``SyntheticEvent
,所有的事件都是它的子类型(源码@types/react/index.d.ts
)。
2. Other Event
除了 Form 还有以下常用的事件对象类型:
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>
过渡事件对象
实用技巧
1. 注释
使用 /** */
注释类型时,在使用相关类型时就会有友好的注释提示,这样可以避免频繁的来回的切文件看使用方式。
Todo 组件
TodoList 组件
2.内置工具泛型
a. Omit
从类型 T 中剔除属性 K
/**
* 源码实现
* Construct a type with the properties of T except for those in type K
*/
type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
有时候你想要复用一个类型,但又不需要其中的所有属性,需要剔除一些属性,使用 Omit 泛型工具就可以很容易的做到(🎈想剔除多个属性,可以参考截图的写法,使用联合类型作为第二类型参数)。
b. Partial
将所有属性都变为可选值。
/**
* 源码实现
* Make all properties in T optional
*/
type Partial<T> = { [P in keyof T]?: T[P] | undefined; }
由上图代码可以看到,给定义了类型 Partial<T>
的 index 常量初始化空对象不会有错误提示。
c. Required
将所有属性都变为必选项
/**
* 源码实现
* Make all properties in T required
*/
type Required<T> = { [P in keyof T]-?: T[P]; }
和 Partial 的功能相反。
d. Pick
从 T 取出一系列 K 的属性
/**
* 源码实现
* From T, pick a set of properties whose keys are in the union K
*/
type Pick<T, K extends keyof T> = { [P in K]: T[P]; }
Pick 可以提高我们接口的复用率,假如我们只需要 IProps 中的 a,b属性,那么就可以使用 Pick 泛型工具来生成一个只支持 a, b属性的新类型。
e. Record
构造具有一组类型为 T 的属性 K 的类型
/**
* 源码实现
* Construct a type with a set of properties K of type T
*/
type Record<K extends keyof any, T> = { [P in K]: T };
使用 Record 来创建一个所有属性值类型一致的类型会更加的优雅,可以节省一些代码量。
f. Exclude
从 T 类型中排除那些赋值给 U 的类型,生成一个新类型
/**
* 源码实现
* Exclude from T those types that are assignable to U
*/
type Exclude<T, U> = T extends U ? never : T;
g. Extract
从 T 类型中提取那些赋值给 U 的类型,生成一个新类型
/**
* 源码实现
* Extract from T those types that are assignable to U
*/
type Extract<T, U> = T extends U ? T : never;