前言
近几年前端对 TypeScript 的呼声越来越高,Ryan Dahl 的新项目 Deno 中 TypeScript 也变成了一个必须要会的技能,知乎上经常见到像『自从用了 TypeScript 之后,再也不想用 JavaScript 了』、『只要你用过 ES6,TypeScript 可以几乎无门槛接入』、『TypeScript可以在任何场景代替 JS』这些类似的回答,抱着听别人说不如自己用的心态逐渐尝试在团队内的一些底层支持的项目中使用 TypeScript。
使用 TypeScript 的编程体验真的是爽到爆,当在键盘上敲下 . 时,后面这一大串的提示真的是满屏幕的幸福,代码质量和效率提升十分明显,再也不想用 JavaScript 了。
在单独使用 TypeScript 时没有太大的坑,但是和一些框架结合使用的话坑还是比较多的,例如使用 React、Vue 这些框架的时候与 TypeScript 的结合会成为一大障碍,需要去查看框架提供的 .d.ts 的声明文件中一些复杂类型的定义。本文主要聊一聊与 React 结合时经常遇到的一些类型定义问题,阅读本文建议对 TypeScript 有一定了解,因为文中对于一些 TypeScript 的基础的知识不会有太过于详细的讲解。
编写第一个 TSX 组件
i
mport React from 'react'
import ReactDOM from 'react-dom'
const App = () => {
return (
<div>Hello world</div>
)
}
ReactDOM.render(<App />, document.getElementById('root')
上述代码运行时会出现以下错误
Cannot find module 'react'
Cannot find module 'react-dom'
错误原因是由于 React 和 React-dom 并不是使用 TS 进行开发的,所以 TS 不知道 React、 React-dom 的类型,以及该模块导出了什么,此时需要引入 .d.ts 的声明文件,比较幸运的是在社区中已经发布了这些常用模块的声明文件 DefinitelyTyped 。
安装 React、 React-dom 类型定义文件
使用 yarn 安装
yarn add @types/react
yarn add @types/react-dom
使用 npm 安装
npm i @types/react -s
npm i @types/react-dom -s
有状态组件开发
我们定义一个 App 有状态组件,props、 state 如下。
Props
props 类型 是否必传
color string 是
size string 否
State
props 类型
count string
使用 TSX 我们可以这样写
import * as React from 'react'
interface IProps {
color: string,
size?: string,
}
interface IState {
count: number,
}
class App extends React.Component<IProps, IState> {
public state = {
count: 1,
}
public render () {
return (
<div>Hello world</div>
)
}
}
TypeScript 可以对 JSX 进行解析,充分利用其本身的静态检查功能,使用泛型进行 Props、 State 的类型定义。定义后在使用 this.state 和 this.props 时可以在编辑器中获得更好的智能提示,并且会对类型进行检查。
那么 Component 的泛型是如何实现的呢,我们可以参考下 React 的类型定义文件 node_modules/@types/react/index.d.ts。
在这里可以看到 Component 这个泛型类, P 代表 Props 的类型, S 代表 State 的类型。
class Component<P, S> {
readonly props: Readonly<{ children?: ReactNode }> & Readonly<P>;
state: Readonly<S>;
}
Component 泛型类在接收到 P , S 这两个泛型变量后,将只读属性 props 的类型声明为交叉类型 Readonly<{ children?: ReactNode }> & Readonly
; 使其支持 children 以及我们声明的 color 、 size 。
通过泛型的类型别名 Readonly 将 props 的所有属性都设置为只读属性。
Readonly 实现源码 node_modules/typescript/lib/lib.es5.d.ts 。
由于 props 属性被设置为只读,所以通过 this.props.size = ‘sm’ 进行更新时候 TS 检查器会进行错误提示,Error:(23, 16) TS2540: Cannot assign to ‘size’ because it is a constant or a read-only property
防止直接更新 state
React的 state 更新需要使用 setState 方法,但是我们经常误操作,直接对 state 的属性进行更新。
this.state.count = 2
开发中有时候会不小心就会写出上面这种代码,执行后 state 并没有更新,我们此时会特别抓狂,心里想着我哪里又错了?
现在有了 TypeScript 我们可以通过将 state ,以及 state 下面的属性都设置为只读类型,从而防止直接更新 state 。
import * as React from 'react'
interface IProps {
color: string,
size?: string,
}
interface IState {
count: number,
}
class App extends React.PureComponent<IProps, IState> {
public readonly state: Readonly<IState> = {
count: 1,
}
public render () {
return (
<div>Hello world</div>
)
}
public componentDidMount () {
this.state.count = 2
}
}
export default App
此时我们直接修改 state 值的时候 TypeScript 会立刻告诉我们错误,Error:(23, 16) TS2540: Cannot assign to ‘count’ because it is a constant or a read-only property. 。
无状态组件开发
Props
props 类型 是否必传
children ReactNode 否
onClick function 是
SFC类型
在 React 的声明文件中 已经定义了一个 SFC 类型,使用这个类型可以避免我们重复定义 children、 propTypes、 contextTypes、 defaultProps、displayName 的类型。
实现源码 node_modules/@types/react/index.d.ts 。
type SFC<P = {}> = StatelessComponent<P>;
interface StatelessComponent<P = {}> {
(props: P & { children?: ReactNode }, context?: any): ReactElement<any> | null;
propTypes?: ValidationMap<P>;
contextTypes?: ValidationMap<any>;
defaultProps?: Partial<P>;
displayName?: string;
}
使用 SFC 进行无状态组件开发。
import { SFC } from 'react'
import { MouseEvent } from 'react'
import * as React from 'react'
interface IProps {
onClick (event: MouseEvent<HTMLDivElement>): void,
}
const Button: SFC<IProps> = ({onClick, children}) => {
return (
<div onClick={onClick}>
{ children }
</div>
)
}
export default Button
事件处理
我们在进行事件注册时经常会在事件处理函数中使用 event 事件对象,例如当使用鼠标事件时我们通过 clientX、clientY 去获取指针的坐标。
大家可以想到直接把 event 设置为 any 类型,但是这样就失去了我们对代码进行静态检查的意义。
function handleEvent (event: any) {
console.log(event.clientY)
}
试想下当我们注册一个 Touch 事件,然后错误的通过事件处理函数中的 event 对象去获取其 clientY 属性的值,在这里我们已经将 event 设置为 any 类型,导致 TypeScript 在编译时并不会提示我们错误, 当我们通过 event.clientY 访问时就有问题了,因为 Touch 事件的 event 对象并没有 clientY 这个属性。
通过 interface 对 event 对象进行类型声明编写的话又十分浪费时间,幸运的是 React 的声明文件提供了 Event 对象的类型声明。
Event 事件对象类型
常用 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> 过渡事件对象
实例:
import { MouseEvent } from 'react'
interface IProps {
onClick (event: MouseEvent<HTMLDivElement>): void,
}
MouseEvent 类型实现源码 node_modules/@types/react/index.d.ts 。
interface SyntheticEvent<T = Element> {
bubbles: boolean;
/**
* A reference to the element on which the event listener is registered.
*/
currentTarget: EventTarget & T;
cancelable: boolean;
defaultPrevented: boolean;
eventPhase: number;
isTrusted: boolean;
nativeEvent: Event;
preventDefault(): void;
isDefaultPrevented(): boolean;
stopPropagation(): void;
isPropagationStopped(): boolean;
persist(): void;
// If you thought this should be `EventTarget & T`, see https://github.com/DefinitelyTyped/DefinitelyTyped/pull/12239
/**
* A reference to the element from which the event was originally dispatched.
* This might be a child element to the element on which the event listener is registered.
*
* @see currentTarget
*/
target: EventTarget;
timeStamp: number;
type: string;
}
interface MouseEvent<T = Element> extends SyntheticEvent<T> {
altKey: boolean;
button: number;
buttons: number;
clientX: number;
clientY: number;
ctrlKey: boolean;
/**
* See [DOM Level 3 Events spec](https://www.w3.org/TR/uievents-key/#keys-modifier). for a list of valid (case-sensitive) arguments to this method.
*/
getModifierState(key: string): boolean;
metaKey: boolean;
nativeEvent: NativeMouseEvent;
pageX: number;
pageY: number;
relatedTarget: EventTarget;
screenX: number;
screenY: number;
shiftKey: boolean;
}
EventTarget 类型实现源码 node_modules/typescript/lib/lib.dom.d.ts 。
interface EventTarget {
addEventListener(type: string, listener: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions): void;
dispatchEvent(evt: Event): boolean;
removeEventListener(type: string, listener?: EventListenerOrEventListenerObject | null, options?: EventListenerOptions | boolean): void;
}
通过源码我们可以看到 MouseEvent<T = Element> 继承 SyntheticEvent,并且通过 T 接收一个 DOM 元素的类型, currentTarget 的类型由 EventTarget & T 组成交叉类型。