TypeScript在React中使用总结

编写第一个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 不知道 React、 React-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

state 类型
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 的类型定义。

那么 Component 的泛型是如何实现的呢,我们可以参考下 React 的类型定义文件 node_modules/@types/react/index.d.ts

P 代表 Props 的类型,S 代表 State 的类型

class Component<P, S> {
  readonly props: Readonly<{ children?: ReactNode }> & Reactonly<P>
  state: Reactonly<S>
}
复制代码

Component泛型类在接收到 PS 这两个范型变量后,将只读属性 props 的类型声明为交叉类型 readonly props: Readonly<{ children?: ReactNode }> & Reactonly<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,以及 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

FC 类型

在React声明文件中,已经定义了一个FC类型,使用这个类型可以避免我们重复定义 propTypescontextTypesdefaultPropsdisplayName 的类型。

实现源码 node_modules/@types/react/index.d.ts

   type FC<P = {}> = FunctionComponent<P>;

    interface FunctionComponent<P = {}> {
        (props: PropsWithChildren<P>, context?: any): ReactElement | null;
        propTypes?: WeakValidationMap<P>;
        contextTypes?: ValidationMap<any>;
        defaultProps?: Partial<P>;
        displayName?: string;
    }
复制代码

使用 FC 进行无状态组件开发

import * as React from 'react'
import { MouseEvent } from 'react'

interface IProps {
  children?: React.ReactNode
  onClick (event: MouseEvent<HTMLDivElement>): void
}

const Button: React.FC<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 事件对象类型

  • 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>;
}
复制代码

可索引类型

实例:

interface StringArray { 
 [index: number]: string
}

let myArray: StringArray
myArray = ["Bob", "Fred"]

let myStr: string = myArray[0]
复制代码

上面例子里,我们定义了 StringArray 接口,它具有索引签名。 这个索引签名表示了当用 number 去索引 StringArray 时会得到 string 类型的返回值。

Typescript 支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。

是因为当使用 number 来索引时,JavaScript 会将它转换成 string 然后再去索引对象。 也就是说用 100(一个number)去索引等同于使用 "100"(一个string)去索引,因此两者需要保持一致。

class Animal { 
  name: string
}
class Dog extends Animal {  
  breed: string
}

// 错误:使用数值型的字符串索引,有时会得到完全不同的Animal
interface NotOkay { 
  [x: number]: Animal  
  [x: string]: Dog
}
复制代码

下面的例子里,name 的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示

interface NumberDictionary { 
  [index: string]: number
  length: number    // 可以,length是number类型  
  name: string    // 错误,`name`的类型与索引类型返回值的类型不匹配
}
复制代码

当然,我们也可以将索引签名设置为只读,这样就可以防止给索引赋值

interface ReadonlyStringArray { 
  readonly [index: number]: string
}

let myArray: ReadonlyStringArray = ["Alice", "Bob"]
myArray[2] = "Mallory" // error!
复制代码

extends

extends 即为扩展、继承。在 ts 中,extends 关键字既可以来扩展已有的类型,也可以对类型进行条件限定。在扩展已有类型时,不可以进行类型冲突的覆盖操作。例如,A为 string,在扩展出的类型中无法将其改为 number

type num = {
  num:number;
}

interface IStrNum extends num {
  str:string;
}

// 与上面等价
type TStrNum = A & {
  str:string;
}
复制代码

ts 中,我们还可以通过条件类型进行一些三目操作:T extends U ? X : Y

type IsEqualType<A , B> = A extends B ? (B extends A ? true : false) : false

type NumberEqualsToString = IsEqualType<number,string> // false
type NumberEqualsToNumber = IsEqualType<number,number> // true
复制代码

函数重载

函数重载的基本语法:

declare function test(a: number): number
declare function test(a: string): string

const resS = test('Hello World')  // resS 被推断出类型为 string
const resN = test(1234)         // resN 被推断出类型为 number
复制代码

这里我们申明了两次?!为什么我不能判断类型或者可选参数呢?后来我遇到这么一个场景:

interface User {
  name: string
  age: number
}

declare function test(para: User | number, flag?: boolean): number
复制代码

在这个 test 函数里,我们的本意可能是当传入参数 para 是 User 时,不传 flag,当传入 para 是 number 时,传入 flag。TypeScript 并不知道这些,当你传入 para 为 User 时,flag 同样允许你传入:

const user = {
  name: 'Jack',
  age: 666
}

// 没有报错,但是与想法违背
const res = test(user, false);
复制代码

使用函数重载能帮助我们实现:

interface User {
  name: string
  age: number
}

declare function test(para: User): number
declare function test(para: number, flag: boolean): number

const user = {
  name: 'Jack',
  age: 666
};

// bingo
// Error: 参数不匹配
const res = test(user, false)
复制代码

实际项目中,你可能要多写几步,如在 class 中:

interface User {
  name: string
  age: number
}

const user = {
  name: 'Jack',
  age: 123
};

class SomeClass {
  /**
   * 注释 1
   */
  public test(para: User): number
  /**
   * 注释 2
   */
  public test(para: number, flag: boolean): number
  public test(para: User | number, flag?: boolean): number {
    // 具体实现
    return 11
  }
}

const someClass = new SomeClass()

// ok
someClass.test(user)
someClass.test(123, false)

// Error
someClass.test(123)
someClass.test(user, false)
复制代码

函数重载的意义在于能够让你知道传入不同的参数得到不同的结果,如果传入的参数不同,但是得到的结果(类型)却相同,那么这里就不要使用函数重载(没有意义)

如果函数的返回值类型相同,那么就不需要使用函数重载。

function func (a: number): number
function func (a: number, b: number): number

// 像这样的是参数个数的区别,我们可以使用可选参数来代替函数重载的定义

function func (a: number, b?: number): number

// 注意第二个参数在类型前边多了一个`?`

// 亦或是一些参数类型的区别导致的
function func (a: number): number
function func (a: string): number

// 这时我们应该使用联合类型来代替函数重载
function func (a: number | string): number
复制代码

工具泛型使用技巧

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 {
  children: React.ReactNode
  color: 'red' | 'blue' | 'yellow',
  onClick (event: MouseEvent<HTMLDivElement>): void,
}

const Button: React.FC<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 Exculde<T,U> = T extends U ? never : T;
复制代码

实例:

type T = Exclude<1|2|3|4|5, 3|4>  // T = 1|2|5 
复制代码

此时 T 类型的值只可以为 123 ,当使用其他值是 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

排除 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 返回值的类型

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

作者:秃头猿
链接:https://juejin.im/post/5e85911cf265da47c8011de9
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

THE END