TypeScript 是 JS 类型的超集,并支持了泛型、类型、命名空间、枚举等特性,弥补了 JS 在大型应用开发中的不足,那么当 TypeScript 与 React 一块儿使用会碰撞出怎样的火花呢?接下来让咱们一块儿探索在 TypeScript2.8+ 版本中编写 React 组件的姿式。前端
近几年前端对 TypeScript 的呼声愈来愈高,Ryan Dahl 的新项目 Deno 中 TypeScript 也变成了一个必需要会的技能,知乎上常常见到像『自从用了 TypeScript 以后,不再想用 JavaScript 了』、『只要你用过 ES6,TypeScript 能够几乎无门槛接入』、『TypeScript能够在任何场景代替 JS』这些相似的回答,抱着听别人说不如本身用的心态逐渐尝试在团队内的一些底层支持的项目中使用 TypeScript。node
使用 TypeScript 的编程体验真的是爽到爆,当在键盘上敲下 .
时,后面这一大串的提示真的是满屏幕的幸福,代码质量和效率提高十分明显,不再想用 JavaScript 了。react
在单独使用 TypeScript 时没有太大的坑,可是和一些框架结合使用的话坑仍是比较多的,例如使用 React、Vue 这些框架的时候与 TypeScript 的结合会成为一大障碍,须要去查看框架提供的 .d.ts 的声明文件中一些复杂类型的定义。本文主要聊一聊与 React 结合时常常遇到的一些类型定义问题,阅读本文建议对 TypeScript 有必定了解,由于文中对于一些 TypeScript 的基础的知识不会有太过于详细的讲解。git
import React from 'react'
import ReactDOM from 'react-dom'
const App = () => {
return (
<div>Hello world</div>
)
}
ReactDOM.render(<App />, document.getElementById('root') 复制代码
上述代码运行时会出现如下错误github
Cannot find module 'react'
typescript
Cannot find module 'react-dom'
shell
错误缘由是因为 React
和 React-dom
并非使用 TS 进行开发的,因此 TS 不知道 React
、 React-dom
的类型,以及该模块导出了什么,此时须要引入 .d.ts 的声明文件,比较幸运的是在社区中已经发布了这些经常使用模块的声明文件 DefinitelyTyped 。npm
React
、 React-dom
类型定义文件yarn add @types/react
yarn add @types/react-dom
复制代码
npm i @types/react -s
npm i @types/react-dom -s
复制代码
咱们定义一个 App 有状态组件,props
、 state
以下。编程
props | 类型 | 是否必传 |
---|---|---|
color |
string | 是 |
size |
string | 否 |
props | 类型 |
---|---|
count |
string |
使用 TSX 咱们能够这样写promise
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<P>;
使其支持 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 | 类型 | 是否必传 |
---|---|---|
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 事件对象类型:
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>
,而且经过 T
接收一个 DOM
元素的类型, currentTarget
的类型由 EventTarget & T
组成交叉类型。
当咱们定义事件处理函数时有没有更方便定义其函数类型的方式呢?答案是使用 React 声明文件所提供的 EventHandler
类型别名,经过不一样事件的 EventHandler
的类型别名来定义事件处理函数的类型。
EventHandler
类型实现源码 node_modules/@types/react/index.d.ts
。
type EventHandler<E extends SyntheticEvent<any>> = { bivarianceHack(event: E): void }["bivarianceHack"];
type ReactEventHandler<T = Element> = EventHandler<SyntheticEvent<T>>;
type ClipboardEventHandler<T = Element> = EventHandler<ClipboardEvent<T>>;
type DragEventHandler<T = Element> = EventHandler<DragEvent<T>>;
type FocusEventHandler<T = Element> = EventHandler<FocusEvent<T>>;
type FormEventHandler<T = Element> = EventHandler<FormEvent<T>>;
type ChangeEventHandler<T = Element> = EventHandler<ChangeEvent<T>>;
type KeyboardEventHandler<T = Element> = EventHandler<KeyboardEvent<T>>;
type MouseEventHandler<T = Element> = EventHandler<MouseEvent<T>>;
type TouchEventHandler<T = Element> = EventHandler<TouchEvent<T>>;
type PointerEventHandler<T = Element> = EventHandler<PointerEvent<T>>;
type UIEventHandler<T = Element> = EventHandler<UIEvent<T>>;
type WheelEventHandler<T = Element> = EventHandler<WheelEvent<T>>;
type AnimationEventHandler<T = Element> = EventHandler<AnimationEvent<T>>;
type TransitionEventHandler<T = Element> = EventHandler<TransitionEvent<T>>;
复制代码
EventHandler
接收 E
,其表明事件处理函数中 event
对象的类型。
bivarianceHack
为事件处理函数的类型定义,函数接收一个 event
对象,而且其类型为接收到的泛型变量 E
的类型, 返回值为 void
。
实例:
interface IProps {
onClick : MouseEventHandler<HTMLDivElement>,
}
复制代码
在作异步操做时咱们常用 async
函数,函数调用时会 return
一个 Promise
对象,可使用 then
方法添加回调函数。
Promise<T>
是一个泛型类型,T
泛型变量用于肯定使用 then
方法时接收的第一个回调函数(onfulfilled)的参数类型。
实例:
interface IResponse<T> {
message: string,
result: T,
success: boolean,
}
async function getResponse (): Promise<IResponse<number[]>> {
return {
message: '获取成功',
result: [1, 2, 3],
success: true,
}
}
getResponse()
.then(response => {
console.log(response.result)
})
复制代码
咱们首先声明 IResponse
的泛型接口用于定义 response
的类型,经过 T
泛型变量来肯定 result
的类型。
而后声明了一个 异步函数 getResponse
而且将函数返回值的类型定义为 Promise<IResponse<number[]>>
。
最后调用 getResponse
方法会返回一个 promise
类型,经过 then
调用,此时 then
方法接收的第一个回调函数的参数 response
的类型为,{ message: string, result: number[], success: boolean}
。
Promise<T>
实现源码 node_modules/typescript/lib/lib.es5.d.ts
。
interface Promise<T> {
/** * Attaches callbacks for the resolution and/or rejection of the Promise. * @param onfulfilled The callback to execute when the Promise is resolved. * @param onrejected The callback to execute when the Promise is rejected. * @returns A Promise for the completion of which ever callback is executed. */
then<TResult1 = T, TResult2 = never>(onfulfilled?: ((value: T) => TResult1 | PromiseLike<TResult1>) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike<TResult2>) | undefined | null): Promise<TResult1 | TResult2>; /** * Attaches a callback for only the rejection of the Promise. * @param onrejected The callback to execute when the Promise is rejected. * @returns A Promise for the completion of the callback. */ catch<TResult = never>(onrejected?: ((reason: any) => TResult | PromiseLike<TResult>) | undefined | null): Promise<T | TResult>; } 复制代码
通常咱们都是先定义类型,再去赋值使用,可是使用 typeof
咱们能够把使用顺序倒过来。
const options = {
a: 1
}
type Options = typeof options
复制代码
限制 props.color
的值只能够是字符串 red
、blue
、yellow
。
interface IProps {
color: 'red' | 'blue' | 'yellow',
}
复制代码
限制 props.index
的值只能够是数字 0
、 1
、 2
。
interface IProps {
index: 0 | 1 | 2,
}
复制代码
Partial
将全部的 props
属性都变为可选值Partial
实现源码 node_modules/typescript/lib/lib.es5.d.ts
type Partial<T> = { [P in keyof T]?: T[P] };
复制代码
上面代码的意思是 keyof T
拿到 T
全部属性名, 而后 in
进行遍历, 将值赋给 P
, 最后 T[P]
取得相应属性的值,中间的 ?
用来进行设置为可选值。
若是 props
全部的属性值都是可选的咱们能够借助 Partial
这样实现。
import { MouseEvent } from 'react'
import * as React from 'react'
interface IProps {
color: 'red' | 'blue' | 'yellow',
onClick (event: MouseEvent<HTMLDivElement>): void,
}
const Button: SFC<Partial<IProps>> = ({onClick, children, color}) => {
return (
<div onClick={onClick}>
{ children }
</div>
)
复制代码
Required
将全部 props
属性都设为必填项Required
实现源码 node_modules/typescript/lib/lib.es5.d.ts
。
type Required<T> = { [P in keyof T]-?: T[P] };
复制代码
看到这里,小伙伴们可能有些疑惑, -?
是作什么的,其实 -?
的功能就是把可选属性的 ?
去掉使该属性变成必选项,对应的还有 +?
,做用与 -?
相反,是把属性变为可选项。
TypeScript2.8引入了条件类型,条件类型能够根据其余类型的特性作出类型的判断。
T extends U ? X : Y
复制代码
原先
interface Id { id: number, /* other fields */ }
interface Name { name: string, /* other fields */ }
declare function createLabel(id: number): Id;
declare function createLabel(name: string): Name;
declare function createLabel(name: string | number): Id | Name;
复制代码
使用条件类型
type IdOrName<T extends number | string> = T extends number ? Id : Name;
declare function createLabel<T extends number | string>(idOrName: T): T extends number ? Id : Name;
复制代码
从 T
中排除那些能够赋值给 U
的类型。
Exclude
实现源码 node_modules/typescript/lib/lib.es5.d.ts
。
type Exclude<T, U> = T extends U ? never : T;
复制代码
实例:
type T = Exclude<1|2|3|4|5, 3|4> // T = 1|2|5
复制代码
此时 T
类型的值只能够为 1
、2
、 5
,当使用其余值是 TS 会进行错误提示。
Error:(8, 5) TS2322: Type '3' is not assignable to type '1 | 2 | 5'.
从 T
中提取那些能够赋值给 U
的类型。
Extract实现源码 node_modules/typescript/lib/lib.es5.d.ts
。
type Extract<T, U> = T extends U ? T : never;
复制代码
实例:
type T = Extract<1|2|3|4|5, 3|4> // T = 3|4
复制代码
此时T类型的值只能够为 3
、4
,当使用其余值时 TS 会进行错误提示:
Error:(8, 5) TS2322: Type '5' is not assignable to type '3 | 4'.
从 T
中取出一系列 K
的属性。
Pick
实现源码 node_modules/typescript/lib/lib.es5.d.ts
。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
复制代码
实例:
假如咱们如今有一个类型其拥有 name
、 age
、 sex
属性,当咱们想生成一个新的类型只支持 name
、age
时能够像下面这样:
interface Person {
name: string,
age: number,
sex: string,
}
let person: Pick<Person, 'name' | 'age'> = {
name: '小王',
age: 21,
}
复制代码
将 K
中全部的属性的值转化为 T
类型。
Record
实现源码 node_modules/typescript/lib/lib.es5.d.ts
。
type Record<K extends keyof any, T> = {
[P in K]: T;
};
复制代码
实例:
将 name
、 age
属性所有设为 string
类型。
let person: Record<'name' | 'age', string> = {
name: '小王',
age: '12',
}
复制代码
从对象 T
中排除 key
是 K
的属性。
因为 TS 中没有内置,因此须要咱们使用 Pick
和 Exclude
进行实现。
type Omit<T, K> = Pick<T, Exclude<keyof T, K>>
复制代码
实例:
排除 name
属性。
interface Person {
name: string,
age: number,
sex: string,
}
let person: Omit<Person, 'name'> = {
age: 1,
sex: '男'
}
复制代码
排除 T
为 null
、undefined
。
NonNullable
实现源码 node_modules/typescript/lib/lib.es5.d.ts
。
type NonNullable<T> = T extends null | undefined ? never : T;
复制代码
实例:
type T = NonNullable<string | string[] | null | undefined>; // string | string[]
复制代码
获取函数 T
返回值的类型。。
ReturnType
实现源码 node_modules/typescript/lib/lib.es5.d.ts
。
type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R ? R : any;
复制代码
infer R
至关于声明一个变量,接收传入函数的返回值类型。
实例:
type T1 = ReturnType<() => string>; // string
type T2 = ReturnType<(s: string) => void>; // void
复制代码