资讯专栏INFORMATION COLUMN

TypeScript在React中使用总结

lixiang / 2500人阅读

摘要:事件处理我们在进行事件注册时经常会在事件处理函数中使用事件对象,例如当使用鼠标事件时我们通过去获取指针的坐标。接收,其代表事件处理函数中对象的类型。小王没有内置从对象中排除是的属性。

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 (

  
Hello world
) } ReactDOM.render(, 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 {
  public state = {
    count: 1,
  }
  public render () {
    return (
      
Hello world
) } }

TypeScript 可以对 JSX 进行解析,充分利用其本身的静态检查功能,使用泛型进行 PropsState 的类型定义。定义后在使用 this.statethis.props 时可以在编辑器中获得更好的智能提示,并且会对类型进行检查。

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

在这里可以看到 Component 这个泛型类, P 代表 Props 的类型, S 代表 State 的类型。

class Component {

    readonly props: Readonly<{ children?: ReactNode }> & Readonly

; state: Readonly; }

Component 泛型类在接收到 PS 这两个泛型变量后,将只读属性 props 的类型声明为交叉类型 Readonly<{ children?: ReactNode }> & Readonly

; 使其支持 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 {
  public readonly state: Readonly = {
    count: 1,
  }
  public render () {
    return (
      
Hello world
) } 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

= StatelessComponent

; interface StatelessComponent

{ (props: P & { children?: ReactNode }, context?: any): ReactElement | null; propTypes?: ValidationMap

; contextTypes?: ValidationMap; defaultProps?: Partial

; displayName?: string; }

使用 SFC 进行无状态组件开发。

import { SFC } from "react"
import { MouseEvent } from "react"
import * as React from "react"
interface IProps {
  onClick (event: MouseEvent): void,
}
const Button: SFC = ({onClick, children}) => {
  return (
    
{ children }
) } 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 剪贴板事件对象

DragEvent 拖拽事件对象

ChangeEvent Change 事件对象

KeyboardEvent 键盘事件对象

MouseEvent 鼠标事件对象

TouchEvent 触摸事件对象

WheelEvent 滚轮事件对象

AnimationEvent 动画事件对象

TransitionEvent 过渡事件对象

实例:

import { MouseEvent } from "react"

interface IProps {

  onClick (event: MouseEvent): void,
}

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

interface SyntheticEvent {
        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 extends SyntheticEvent {
        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 继承 SyntheticEvent,并且通过 T 接收一个 DOM 元素的类型, currentTarget 的类型由 EventTarget & T 组成交叉类型。

事件处理函数类型

当我们定义事件处理函数时有没有更方便定义其函数类型的方式呢?答案是使用 React 声明文件所提供的 EventHandler 类型别名,通过不同事件的 EventHandler 的类型别名来定义事件处理函数的类型。

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

    type EventHandler> = { bivarianceHack(event: E): void }["bivarianceHack"];
    type ReactEventHandler = EventHandler>;
    type ClipboardEventHandler = EventHandler>;
    type DragEventHandler = EventHandler>;
    type FocusEventHandler = EventHandler>;
    type FormEventHandler = EventHandler>;
    type ChangeEventHandler = EventHandler>;
    type KeyboardEventHandler = EventHandler>;
    type MouseEventHandler = EventHandler>;
    type TouchEventHandler = EventHandler>;
    type PointerEventHandler = EventHandler>;
    type UIEventHandler = EventHandler>;
    type WheelEventHandler = EventHandler>;
    type AnimationEventHandler = EventHandler>;
    type TransitionEventHandler = EventHandler>;

EventHandler 接收 E ,其代表事件处理函数中 event 对象的类型。

bivarianceHack 为事件处理函数的类型定义,函数接收一个 event 对象,并且其类型为接收到的泛型变量 E 的类型, 返回值为 void

实例:

interface IProps {
  onClick : MouseEventHandler,
}
Promise 类型

在做异步操作时我们经常使用 async 函数,函数调用时会 return 一个 Promise 对象,可以使用 then 方法添加回调函数。

Promise 是一个泛型类型,T 泛型变量用于确定使用 then 方法时接收的第一个回调函数(onfulfilled)的参数类型。

实例:

interface IResponse {
  message: string,
  result: T,
  success: boolean,
}
async function getResult (): Promise> {
  return {
    message: "获取成功",
    result: [1, 2, 3],
    success: true,
  }
}
getResult()
  .then(result => {
    console.log(result.result)
  })

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

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

最后调用 getResult 方法会返回一个 promise 类型,通过 .then 调用,此时 .then 方法接收的第一个回调函数的参数 result 的类型为,{ message: string, result: number[], success: boolean}

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

interface Promise {
    /**
     * 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(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise;
    /**
     * 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(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise;
}
工具泛型使用技巧
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 = { [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): void,
}
const Button: SFC> = ({onClick, children, color}) => {
  return (
    
{ children }
)
使用 Required 将所有 props 属性都设为必填项

Required 实现源码 node_modules/typescript/lib/lib.dom.d.ts

type Required = { [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 ? Id : Name;
declare function createLabel(idOrName: T): T extends number ? Id : Name;
Exclude

T 中排除那些可以赋值给 U 的类型。

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

type Exclude = 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 的类型。

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

type Extract = 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 的属性。

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

type Pick = {
    [P in K]: T[P];
};

实例:

假如我们现在有一个类型其拥有 nameagesex 属性,当我们想生成一个新的类型只支持 nameage 时可以像下面这样:

interface Person {
  name: string,
  age: number,
  sex: string,
}
let person: Pick = {
  name: "小王",
  age: 21,
}
Record

K 中所有的属性的值转化为 T 类型。

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

type Record = {
    [P in K]: T;
};

实例:

nameage 属性全部设为 string 类型。

let person: Record<"name" | "age", string> = {
  name: "小王",
  age: "12",
}
Omit(没有内置)

从对象 T 中排除 key K 的属性。

由于 TS 中没有内置,所以需要我们使用 PickExclude 进行实现。

type Omit = Pick>

实例:

排除 name 属性。

interface Person {
  name: string,
  age: number,
  sex: string,
}


let person: Omit = {
  age: 1,
  sex: "男"
}
NonNullable

排除 Tnullundefined

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

type NonNullable = T extends null | undefined ? never : T;

实例:

type T = NonNullable; // string | string[]
ReturnType

获取函数 T 返回值的类型。。

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

type ReturnType any> = T extends (...args: any[]) => infer R ? R : any;

infer R 相当于声明一个变量,接收传入函数的返回值类型。

实例:

type T1 = ReturnType<() => string>; // string
type T2 = ReturnType<(s: string) => void>; // void

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/98008.html

相关文章

  • React项目从Javascript到Typescript的迁移经验总结

    摘要:面对越来越火的,我们公司今年也逐渐开始拥抱。综上所述,我个人觉得是要删除相关的东西,降低项目复杂度。但是有一个例外情况。这个配置项有三个值可选择,分别是和。模式会生成,在使用前不需要再进行转换操作了,输出文件的扩展名为。 抛转引用 现在越来越多的项目放弃了javascript,而选择拥抱了typescript,就比如我们熟知的ant-design就是其中之一。面对越来越火的typesc...

    zhisheng 评论0 收藏0
  • 两年React老兵的总结 - 类型检查篇

    摘要:系列引言最近准备培训新人为了方便新人较快入手开发并编写高质量的组件代码我根据自己的实践经验对组件设计的相关实践和规范整理了一些文档将部分章节分享了出来由于经验有限文章可能会有某些错误希望大家指出互相交流由于篇幅太长所以拆分为几篇文章主要有以 系列引言 最近准备培训新人, 为了方便新人较快入手 React 开发并编写高质量的组件代码, 我根据自己的实践经验对React 组件设计的相关实践...

    scola666 评论0 收藏0
  • 「每日一瞥

    摘要:即使中没有错误,仍然会执行,这一点一般都是知道的。我们认为这是正确的前进道路,兼具战略性和务实性降低使用门槛开发人员迁移到的障碍之一是从到的并不轻松的迁移。下一步将通过一系列功能和插件为的平滑过渡提供支持,并以此回馈社区。 showImg(https://segmentfault.com/img/remote/1460000017516912?w=1200&h=630); useSt...

    XboxYan 评论0 收藏0
  • React+TypeScript+Mobx+AntDesignMobile进行移动端项目搭建

    摘要:通过装饰器或者利用时调用的函数来进行使用下面代码中当或者发生变化时,会监听数据变化确保通过触发方法自动更新。只能影响正在运行的函数,而无法影响当前函数调用的异步操作参考官方文档用法装饰器函数遵循中标准的绑定规则。 前言: 本文基于React+TypeScript+Mobx+AntDesignMobile技术栈,使用Create-React-App脚手架进行一个移动端项目搭建,主要介绍项...

    lindroid 评论0 收藏0
  • TypeScript极速完全入门-3ts结合react进行项目开发

    摘要:前面我们已经说了大部分的核心内容,接下来我们就说说如何用开发实际项目。因为和结合很紧密,资料也很多,而且我会找机会专门说下这方面的知识,所以我们将重点放到如何用结合上来。所以前面打牢基础,现在我们开始实际组建工作流。 前面我们已经说了大部分typescript的核心内容,接下来我们就说说如何用typescript开发实际项目。 因为angular和typescript结合很紧密,资料也...

    arashicage 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<