TypeScript 是 JS 类型的超集,并支持了泛型、类型、命名空间、枚举等特性,弥补了 JS 在大型应用开发中的不足,本文主要探索在 TypeScript版本中编写 React 组件的姿式。javascript
在动手将TypeScript融合进现有的React项目以前,先看一下create-react-app
是怎么作的。html
首先建立一个叫作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
是强制使用的入口文件。打开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
错误缘由是因为 React
和 React-dom
并非使用 TS 进行开发的,因此 TS 不知道 React
、 React-dom
的类型,以及该模块导出了什么,此时须要引入 .d.ts 的声明文件,比较幸运的是在社区中已经发布了这些经常使用模块的声明文件 DefinitelyTyped 。
因此若是咱们的工程不是使用
create-react-app
建立的,记得npm install @types/xxx
。
若是一个目录下存在一个tsconfig.json
文件,那么它意味着这个目录是TypeScript项目的根目录。tsconfig.json
文件中指定了用来编译这个项目的根文件和编译选项。
执行tsc --init
生成本身的tsconfig.json
配置文件,示例以下。
{
"compilerOptions": {
"outDir": "./dist/",
"sourceMap": true,
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react"
},
"include": [
"./src/**/*"
]
}
复制代码
noImplicitAny
标志是 false
(默认值)时, 若是编译器没法根据变量的用途推断出变量的类型,它就会悄悄的把变量类型默认为 any
。这就是隐式 any的含义。当 noImplicitAny
标志是 true
而且 TypeScript 编译器没法推断出类型时,它仍然会生成 JavaScript 文件。 可是它也会报告一个错误。npm install eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin eslint-plugin-react eslint-config-alloy babel-eslint --save-dev
复制代码
@typescript-eslint/parser
:将 TypeScript 转换为 ESTree,使 eslint 能够识别@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后缀的文件
npm i prettier eslint-config-prettier eslint-plugin-prettier -D
复制代码
prettier
: 格式化规则程序eslint-config-prettier
: 将禁用任何可能干扰现有 prettier
规则的 linting
规则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,慎用
为了让 vscode 的 eslint 插件启用 typescript 支持,须要添加下面的配置到 .vscode/settings.json
中。
"eslint.validate": [
"javascript",
"javascriptreact",
{
"language": "typescript",
"autoFix": true
},
{
"language": "typescriptreact",
"autoFix": true
}
]
复制代码
修改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
当咱们传递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。对于类组件,咱们利用尖括号语法去分别应用咱们的props和state的interface。
export class MyForm extends React.Component<FormProps, FormState> {
...
}
复制代码
注意:在只有state而没有props的状况下,props的位置能够用{}或者object占位,这两个值都表示有效的空对象。
对于纯函数组件,咱们能够直接传递props interface
function MyForm(props: FormProps) {
...
}
复制代码
按照约定,咱们通常会建立一个 **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
事件对象,例如当使用鼠标事件时咱们经过 clientX
、clientY
去获取指针的坐标。
你们能够想到直接把 event
设置为 any
类型,可是这样就失去了咱们对代码进行静态检查的意义。
function handleEvent (event: any) {
console.log(event.clientY)
}
复制代码
试想下当咱们注册一个 Touch
事件,而后错误的经过事件处理函数中的 event
对象去获取其 clientY
属性的值,在这里咱们已经将 event
设置为 any
类型,致使 TypeScript 在编译时并不会提示咱们错误, 当咱们经过 event.clientY
访问时就有问题了,由于 Touch
事件的 event
对象并无 clientY
这个属性。
经过 interface
对 event
对象进行类型声明编写的话又十分浪费时间,幸运的是 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,
}
复制代码
在作异步操做时咱们常用 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
咱们能够把使用顺序倒过来。
const options = {
a: 1
}
type Options = typeof options
复制代码
限制 props.color
的值只能够是字符串 red
、blue
、yellow
。
interface IProps {
color: 'red' | 'blue' | 'yellow',
}
复制代码
限制 props.index
的值只能够是数字 0
、 1
、 2
。
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;
复制代码
从 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
类型的值只能够为 1
、2
、 5
,当使用其余值是 TS 会进行错误提示。
Error:(8, 5) TS2322: Type '3' is not assignable to type '1 | 2 | 5'.
复制代码
从 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类型的值只能够为 3
、4
,当使用其余值时 TS 会进行错误提示:
Error:(8, 5) TS2322: Type '5' is not assignable to type '3 | 4'.
复制代码
从 T
中取出一系列 K
的属性。
Pick
实现源码 node_modules/typescript/lib/lib.es5.d.ts
。
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
复制代码
实例:
假如咱们如今有一个类型其拥有 name
、 age
、 sex
属性,当咱们想生成一个新的类型只支持 name
、age
时能够像下面这样:
interface Person {
name: string,
age: number,
sex: string,
}
let person: Pick<Person, 'name' | 'age'> = {
name: '小王',
age: 21,
}
复制代码
将 K
中全部的属性的值转化为 T
类型。
Record
实现源码 node_modules/typescript/lib/lib.es5.d.ts
。
type Record<K extends keyof any, T> = {
[P in K]: T;
};
复制代码
实例:
将 name
、 age
属性所有设为 string
类型。
let person: Record<'name' | 'age', string> = {
name: '小王',
age: '12',
}
复制代码
从对象 T
中排除 key
是 K
的属性。
因为 TS 中没有内置,因此须要咱们使用 Pick
和 Exclude
进行实现。
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: '男'
}
复制代码
排除 T
为 null
、undefined
。
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[]
复制代码
获取函数 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
复制代码