TypeScript在react中的实践

TypeScript 是 JS 类型的超集,并支持了泛型、类型、命名空间、枚举等特性,弥补了 JS 在大型应用开发中的不足,本文主要探索在 TypeScript版本中编写 React 组件的姿势。

在动手将TypeScript融合进现有的React项目之前,先看一下create-react-app是怎么做的。

从create-react-app中一探究竟

首先创建一个叫做my-app的新工程:

create-react-app my-app --scripts-version=react-scripts-ts
复制代码

react-scripts-ts是一系列适配器,它利用标准的create-react-app工程管道并把TypeScript混入进来。此时的工程结构应如下所示:

my-app/
├─ .gitignore
├─ node_modules/
├─ public/
├─ src/
│  └─ ...
├─ package.json
├─ tsconfig.json
└─ tslint.json
复制代码

注意:

  • tsconfig.json包含了工程里TypeScript特定的配置选项。
  • tslint.json保存了要使用的代码检查器的设置,TSLint。
  • package.json包含了依赖,还有一些命令的快捷方式,如测试命令,预览命令和发布应用的命令。
  • public包含了静态资源如HTML页面或图片。除了index.html文件外,其它的文件都可以删除。
  • src包含了TypeScript和CSS源码。index.tsx是强制使用的入口文件。

@types

打开package.json文件,查看devDependencies,发现一系列@types文件,如下:

"devDependencies": {
    "@types/node": "^12.6.9",
    "@types/react": "^16.8.24",
    "@types/react-dom": "^16.8.5",
    "typescript": "^3.5.3"
}
复制代码

使用@types/前缀表示我们额外要获取React和React-DOM的声明文件(关于声明文件,参考文章)。 通常当你导入像"react"这样的路径,它会查看react包; 然而,并不是所有的包都包含了声明文件,所以TypeScript还会查看@types/react包。

如果没有这些@types文件,我们在TSX 组件中,引入React 或者ReactDOM 会报错:

Cannot find module 'react'

Cannot find module 'react-dom'

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

所以如果我们的工程不是使用create-react-app创建的,记得npm install @types/xxx

tsconfig.json

如果一个目录下存在一个tsconfig.json文件,那么它意味着这个目录是TypeScript项目的根目录。tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项。

执行tsc --init生成自己的tsconfig.json配置文件,示例如下。

{
    "compilerOptions": {
        "outDir": "./dist/",
        "sourceMap": true,
        "noImplicitAny": true,
        "module": "commonjs",
        "target": "es5",
        "jsx": "react"
    },
    "include": [
        "./src/**/*"
    ]
}
复制代码
  • target:默认情况下,编译目标是 es5,如果你只想发布到兼容 es6 的浏览器中,也可以把它配置为 es6。 不过,如果配置为 es6,那么一些老的浏览器(如 IE )中就会抛出 Syntax Error 错误。
  • noImplicitAny :当 noImplicitAny 标志是 false(默认值)时, 如果编译器无法根据变量的用途推断出变量的类型,它就会悄悄的把变量类型默认为 any。这就是隐式 any的含义。当 noImplicitAny 标志是 true 并且 TypeScript 编译器无法推断出类型时,它仍然会生成 JavaScript 文件。 但是它也会报告一个错误

使用eslint进行代码检查

安装依赖

npm install eslint typescript-eslint-parser eslint-plugin-typescript eslint-config-alloy  babel-eslint --save-dev
复制代码

创建配置文件.eslintrc.js并写入规则

module.exports = {
    parser: 'typescript-eslint-parser',
    plugins: [
        'typescript'
    ],
    rules: {
        // @fixable 必须使用 === 或 !==,禁止使用 == 或 !=,与 null 比较时除外
        'eqeqeq': [
            'error',
            'always',
            {
                null: 'ignore'
            }
        ],
        // 类和接口的命名必须遵守帕斯卡命名法,比如 PersianCat
        'typescript/class-name-casing': 'error'
    }
}
复制代码

这里使用的是 AlloyTeam ESLint 的 TypeScript 规则

然后在package.json中增加配置,检查src目录下所有的ts文件。

"scripts": {
	"eslint": "eslint src --ext .ts,.js,.tsx,.jsx"
}
复制代码

此时执行 npm run eslint 即会检查 src 目录下的所有.ts,.js,.tsx,.jsx后缀的文件

在webpack中配置

修改webpack.config.js文件

module.exports = {
    entry: "./src/index.tsx",
    output: {
        filename: "bundle.js",
        path: __dirname + "/dist"
    },

    devtool: "source-map",

    resolve: {
        extensions: [".ts", ".tsx", ".js", ".json"]
    },

    module: {
        rules: [
            { test: /\.tsx?$/, loader: "awesome-typescript-loader" },
            { enforce: "pre", test: /\.js$/, loader: "source-map-loader" }
        ]
    },
    externals: {
        "react": "React",
        "react-dom": "ReactDOM"
    },
};
复制代码

awesome-typescript-loader是用来编译ts文件得,也可以使用ts-loader,两者之间得区别,请参考:awesome-typescript-loader & ts-loader

组件开发

有状态组件开发

定义interface

当我们传递props到组件中去的时候,如果想要使props应用interface,那就会强制要求我们传递的props必须遵循interface的结构,确保成员都有被声明,同时也会阻止未期望的props被传递下去。

interface可以定义在组件的外部或是一个独立文件,可以像这样定义一个interface

interface FormProps {
    first_name: string;
    last_name: string;
    age: number;
    agreetoterms?: boolean;
}
复制代码

这里我们创建了一个FormProps接口,包含一些值。我们也可以给组件的state应用一个interface

interface FormState {
    submitted?: boolean;
    full_name: string;
    age: number;
}
复制代码

给组件应用interface

我们既可以给类组件也可以给无状态组件应用interface。对于类组件,我们利用尖括号语法去分别应用我们的props和state的interface。

export class MyForm extends React.Component {
	...
}
复制代码

注意:在只有state而没有props的情况下,props的位置可以用{}或者object占位,这两个值都表示有效的空对象。

对于纯函数组件,我们可以直接传递props interface

function MyForm(props: FormProps) {
	...
}
复制代码

引入interface

按照约定,我们一般会创建一个 **src/types/**目录来将你的所有interface分组:

// src/types/index.tsx
export interface FormProps {
    first_name: string;
    last_name: string;
    age: number;
    agreetoterms?: boolean;
}
复制代码

然后引入组件所需要的interface

// src/components/MyForm.tsx
import React from 'react';
import { StoreState } from '../types/index';
...
复制代码

无状态组件开发

无状态组件也被称为展示组件,如果一个展示组件没有内部的state可以被写为纯函数组件。 如果写的是函数组件,在@types/react中定义了一个类型type SFC

= StatelessComponent

;。我们写函数组件的时候,能指定我们的组件为SFC或者StatelessComponent。这个里面已经预定义了children等,所以我们每次就不用指定类型children的类型了。

实现源码 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 React, { ReactNode, SFC } from 'react';
import style from './step-complete.less';

export interface IProps  {
  title: string | ReactNode;
  description: string | ReactNode;
}
const StepComplete:SFC = ({ title, description, children }) => {
  return (
    
{title}
{description}
{children}
); }; export default StepComplete; 复制代码

事件处理

我们在进行事件注册时经常会在事件处理函数中使用 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,
}
复制代码

Promise 类型

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

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

interface IResponse {
  message: string,
  result: T,
  success: boolean,
}
async function getResponse (): Promise> {
  return {
    message: '获取成功',
    result: [1, 2, 3],
    success: true,
  }
}

getResponse()
  .then(response => {
    console.log(response.result)
  })
复制代码

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

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

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

泛型组件

工具泛型使用技巧

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 = { [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.es5.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 = 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 的属性。

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 中排除 keyK 的属性。

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


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

你可能感兴趣的:(【React.js点滴知识,】)