TypeScript 在 React 中使用总结

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

编写第一个 TSX 组件

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

错误缘由是因为 ReactReact-dom 并非使用 TS 进行开发的,因此 TS 不知道 ReactReact-dom 的类型,以及该模块导出了什么,此时须要引入 .d.ts 的声明文件,比较幸运的是在社区中已经发布了这些经常使用模块的声明文件 DefinitelyTypednpm

安装 ReactReact-dom 类型定义文件

使用 yarn 安装
yarn add @types/react

yarn add @types/react-dom

复制代码
使用 npm 安装
npm i @types/react -s

npm i @types/react-dom -s

复制代码

有状态组件开发

咱们定义一个 App 有状态组件,propsstate 以下。编程

Props
props 类型 是否必传
color string
size string
State
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 进行解析,充分利用其自己的静态检查功能,使用泛型进行 PropsState 的类型定义。定义后在使用 this.statethis.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 泛型类在接收到 PS 这两个泛型变量后,将只读属性 props 的类型声明为交叉类型 Readonly<{ children?: ReactNode }> & Readonly<P>; 使其支持 children 以及咱们声明的 colorsize

经过泛型的类型别名 Readonlyprops 的全部属性都设置为只读属性。

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 类型,使用这个类型能够避免咱们重复定义 childrenpropTypescontextTypesdefaultPropsdisplayName 的类型。

实现源码 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 事件对象,例如当使用鼠标事件时咱们经过 clientXclientY 去获取指针的坐标。

你们能够想到直接把 event 设置为 any 类型,可是这样就失去了咱们对代码进行静态检查的意义。

function handleEvent (event: any) {
  console.log(event.clientY)
}

复制代码

试想下当咱们注册一个 Touch 事件,而后错误的经过事件处理函数中的 event 对象去获取其 clientY 属性的值,在这里咱们已经将 event 设置为 any 类型,致使 TypeScript 在编译时并不会提示咱们错误, 当咱们经过 event.clientY 访问时就有问题了,由于 Touch 事件的 event 对象并无 clientY 这个属性。

经过 interfaceevent 对象进行类型声明编写的话又十分浪费时间,幸运的是 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>,而且经过 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>,
}

复制代码

Promise 类型

在作异步操做时咱们常用 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

通常咱们都是先定义类型,再去赋值使用,可是使用 typeof 咱们能够把使用顺序倒过来。

const options = {
  a: 1
}
type Options = typeof options

复制代码
使用字符串字面量类型限制值为固定的字符串参数

限制 props.color 的值只能够是字符串 redblueyellow

interface IProps {
  color: 'red' | 'blue' | 'yellow',
}

复制代码
使用数字字面量类型限制值为固定的数值参数

限制 props.index 的值只能够是数字 012

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;

复制代码
Exclude<T,U>

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 类型的值只能够为 125 ,当使用其余值是 TS 会进行错误提示。

Error:(8, 5) TS2322: Type '3' is not assignable to type '1 | 2 | 5'.

Extract<T,U>

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类型的值只能够为 34 ,当使用其余值时 TS 会进行错误提示:

Error:(8, 5) TS2322: Type '5' is not assignable to type '3 | 4'.

Pick<T,K>

T 中取出一系列 K 的属性。

Pick 实现源码 node_modules/typescript/lib/lib.es5.d.ts

type Pick<T, K extends keyof T> = {
    [P in K]: T[P];
};

复制代码

实例:

假如咱们如今有一个类型其拥有 nameagesex 属性,当咱们想生成一个新的类型只支持 nameage 时能够像下面这样:

interface Person {
  name: string,
  age: number,
  sex: string,
}
let person: Pick<Person, 'name' | 'age'> = {
  name: '小王',
  age: 21,
}

复制代码
Record<K,T>

K 中全部的属性的值转化为 T 类型。

Record 实现源码 node_modules/typescript/lib/lib.es5.d.ts

type Record<K extends keyof any, T> = {
    [P in K]: T;
};

复制代码

实例:

nameage 属性所有设为 string 类型。

let person: Record<'name' | 'age', string> = {
  name: '小王',
  age: '12',
}

复制代码
Omit<T,K>(没有内置)

从对象 T 中排除 keyK 的属性。

因为 TS 中没有内置,因此须要咱们使用 PickExclude 进行实现。

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: '男'
}

复制代码
NonNullable <T>

排除 Tnullundefined

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[]

复制代码
ReturnType<T>

获取函数 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
复制代码