TypeScript 是 JS 类型的超集,并支持了泛型、类型、命名空间、枚举等特性,弥补了 JS 在大型应用开发中的不足,本文主要探索在 TypeScript版本中编写 React 组件的姿势。
在动手将TypeScript融合进现有的React项目之前,先看一下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
是强制使用的入口文件。打开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'
错误原因是由于 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 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.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可以被写为纯函数组件。 如果写的是函数组件,在 = StatelessComponent ;@types/react
中定义了一个类型type SFC
。我们写函数组件的时候,能指定我们的组件为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
事件对象,例如当使用鼠标事件时我们通过 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 事件对象类型
常用 Event 事件对象类型:
ClipboardEvent
剪贴板事件对象DragEvent
拖拽事件对象ChangeEvent
Change 事件对象KeyboardEvent
键盘事件对象MouseEvent
鼠标事件对象TouchEvent
触摸事件对象WheelEvent
滚轮事件对象AnimationEvent
动画事件对象TransitionEvent
过渡事件对象实例:
import { MouseEvent } from 'react';
interface IProps {
onClick (event: MouseEvent): void,
}
复制代码
在做异步操作时我们经常使用 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
我们可以把使用顺序倒过来。
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 = { [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;
复制代码
从 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
类型的值只可以为 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 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 = {
[P in K]: T[P];
};
复制代码
实例:
假如我们现在有一个类型其拥有 name
、 age
、 sex
属性,当我们想生成一个新的类型只支持 name
、age
时可以像下面这样:
interface Person {
name: string,
age: number,
sex: string,
}
let person: Pick = {
name: '小王',
age: 21,
}
复制代码
将 K
中所有的属性的值转化为 T
类型。
Record
实现源码 node_modules/typescript/lib/lib.es5.d.ts
。
type Record = {
[P in K]: T;
};
复制代码
实例:
将 name
、 age
属性全部设为 string
类型。
let person: Record<'name' | 'age', string> = {
name: '小王',
age: '12',
}
复制代码
从对象 T
中排除 key
是 K
的属性。
由于 TS 中没有内置,所以需要我们使用 Pick
和 Exclude
进行实现。
type Omit = Pick>
复制代码
实例:
排除 name
属性。
interface Person {
name: string,
age: number,
sex: string,
}
let person: Omit = {
age: 1,
sex: '男'
}
复制代码
排除 T
为 null
、undefined
。
NonNullable
实现源码 node_modules/typescript/lib/lib.es5.d.ts
。
type NonNullable = T extends null | undefined ? never : T;
复制代码
实例:
type T = NonNullable; // string | string[]
复制代码
获取函数 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
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。