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