TypeScript在react中的实践

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

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

从create-react-app中一探究竟

首先建立一个叫作my-app的新工程:java

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

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

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

注意:react

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

@types

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

"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包。git

若是没有这些@types文件,咱们在TSX 组件中,引入React 或者ReactDOM 会报错:es6

Cannot find module 'react'github

Cannot find module 'react-dom'web

错误缘由是因为 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进行代码检查

安装 eslint 依赖

npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy  babel-eslint --save-dev
复制代码
  1. @typescript-eslint/parser:将 TypeScript 转换为 ESTree,使 eslint 能够识别
  2. @typescript-eslint/eslint-plugin:只是一个能够打开或关闭的规则列表

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

module.exports = {
    parser: "@typescript-eslint/parser",
 	extends: ["plugin:@typescript-eslint/recommended", "react-app"],
  	plugins: ["@typescript-eslint", "react"],
  	rules: {
        // ...
    }
}
复制代码

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

而后在package.json中增长配置,检查src目录下全部的ts文件。

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

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

安装 prettier 依赖

npm i prettier eslint-config-prettier eslint-plugin-prettier -D
复制代码
  1. prettier: 格式化规则程序
  2. eslint-config-prettier: 将禁用任何可能干扰现有 prettier 规则的 linting 规则
  3. eslint-plugin-prettier: 将做为ESlint 的一部分运行 Prettier分析。
module.exports = {
  parser: '@typescript-eslint/parser',
  extends: [
    'plugin:@typescript-eslint/recommended',
    'plugin:prettier/recommended',
  ],
  plugins: ['@typescript-eslint', 'react'],
  rules: {},
};
复制代码

旧项目中引入Prettier会致使超级多的error,慎用

Visual Studio Code 集成 ESLint 与 Prettier

为了让 vscode 的 eslint 插件启用 typescript 支持,须要添加下面的配置到 .vscode/settings.json 中。

"eslint.validate": [
    "javascript",
    "javascriptreact",
    {
      "language": "typescript",
      "autoFix": true
    },
    {
      "language": "typescriptreact",
      "autoFix": true
    }
]
复制代码

在webpack中配置loader

修改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<FormProps, FormState> {
	...
}
复制代码

注意:在只有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<P = {}> = StatelessComponent<P>;。咱们写函数组件的时候,能指定咱们的组件为SFC或者StatelessComponent。这个里面已经预约义了children等,因此咱们每次就不用指定类型children的类型了。

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

export interface IProps  {
  title: string | ReactNode;
  description: string | ReactNode;
}
const StepComplete:SFC<IProps> = ({ title, description, children }) => {
  return (
    <div className={style.complete}> <div className={style.completeTitle}> {title} </div> <div className={style.completeSubTitle}> {description} </div> <div> {children} </div> </div>
  );
};
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<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,
}
复制代码

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}

泛型组件

工具泛型使用技巧

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
复制代码
相关文章
相关标签/搜索