11

TypeScript 是 JS 类型的超集,并支持了泛型、类型、命名空间、枚举等特性,弥补了 JS 在大型应用开发中的不足,那么当 TypeScript 与 React 一起使用会碰撞出怎样的火花呢?接下来让我们一起探索在 TypeScript2.8+ 版本中编写 React 组件的姿势。

前言

近几年前端对 TypeScript 的呼声越来越高,Ryan Dahl 的新项目 Deno 中 TypeScript 也变成了一个必须要会的技能,知乎上经常见到像『自从用了 TypeScript 之后,再也不想用 JavaScript 了』、『只要你用过 ES6,TypeScript 可以几乎无门槛接入』、『TypeScript可以在任何场景代替 JS』这些类似的回答,抱着听别人说不如自己用的心态逐渐尝试在团队内的一些底层支持的项目中使用 TypeScript。

使用 TypeScript 的编程体验真的是爽到爆,当在键盘上敲下 . 时,后面这一大串的提示真的是满屏幕的幸福,代码质量和效率提升十分明显,再也不想用 JavaScript 了。

在单独使用 TypeScript 时没有太大的坑,但是和一些框架结合使用的话坑还是比较多的,例如使用 React、Vue 这些框架的时候与 TypeScript 的结合会成为一大障碍,需要去查看框架提供的 .d.ts 的声明文件中一些复杂类型的定义。本文主要聊一聊与 React 结合时经常遇到的一些类型定义问题,阅读本文建议对 TypeScript 有一定了解,因为文中对于一些 TypeScript 的基础的知识不会有太过于详细的讲解。

编写第一个 TSX 组件


import 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'

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

安装 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 我们可以这样写


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 getResult (): Promise<IResponse<number[]>> {
  return {
    message: '获取成功',
    result: [1, 2, 3],
    success: true,
  }
}
getResult()
  .then(result => {
    console.log(result.result)
  })

我们首先声明 IResponse 的泛型接口用于定义 response 的类型,通过 T 泛型变量来确定 result 的类型。

然后声明了一个 异步函数 getResult 并且将函数返回值的类型定义为 Promise<IResponse<number[]>>

最后调用 getResult 方法会返回一个 promise 类型,通过 .then 调用,此时 .then 方法接收的第一个回调函数的参数 result 的类型为,{ 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.dom.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.dom.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 = Exclude<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 中排除 key K 的属性。

由于 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

花生毛豆
92 声望2 粉丝

起于前端,但不止于前端。