六、工具泛型
在项目中使用一些工具泛型可以提高我们的开发效率,少写很多类型定义。下面来看看有哪些常见的工具泛型,以及其使用方式。
1. Partial
Partial 作用是将传入的属性变为可选项。适用于对类型结构不明确的情况。它使用了两个关键字:keyof和in,先来看看他们都是什么含义。keyof 可以用来取得接口的所有 key 值:
interface IPerson { name: string; age: number; height: number; } type T = keyof IPerson // T 类型为: "name" | "age" | "number" 复制代码
in关键字可以遍历枚举类型,:
type Person = "name" | "age" | "number" type Obj = { [p in Keys]: any } // Obj类型为: { name: any, age: any, number: any } 复制代码
keyof 可以产生联合类型, in 可以遍历枚举类型, 所以经常一起使用, 下面是Partial工具泛型的定义:
/** * Make all properties in T optional * 将T中的所有属性设置为可选 */ type Partial<T> = { [P in keyof T]?: T[P]; }; 复制代码
这里,keyof T 获取 T 所有属性名, 然后使用 in 进行遍历, 将值赋给 P, 最后 T[P] 取得相应属性的值。中间的?就用来将属性设置为可选。
使用示例如下:
interface IPerson { name: string; age: number; height: number; } const person: Partial<IPerson> = { name: "zhangsan"; } 复制代码
2. Required
Required 的作用是将传入的属性变为必选项,和上面的工具泛型恰好相反,其声明如下:
/** * Make all properties in T required * 将T中的所有属性设置为必选 */ type Required<T> = { [P in keyof T]-?: T[P]; }; 复制代码
可以看到,这里使用-?将属性设置为必选,可以理解为减去问号。适用形式和上面的Partial差不多:
interface IPerson { name?: string; age?: number; height?: number; } const person: Required<IPerson> = { name: "zhangsan"; age: 18; height: 180; } 复制代码
3. Readonly
将T类型的所有属性设置为只读(readonly),构造出来类型的属性不能被再次赋值。Readonly的声明形式如下:
/** * Make all properties in T readonly */ type Readonly<T> = { readonly [P in keyof T]: T[P]; }; 复制代码
使用示例如下:
interface IPerson { name: string; age: number; } const person: Readonly<IPerson> = { name: "zhangsan", age: 18 } person.age = 20; // Error: cannot reassign a readonly property 复制代码
可以看到,通过 Readonly 将IPerson的属性转化成了只读,不能再进行赋值操作。
4. Pick<T, K extends keyof T>
从T类型中挑选部分属性K来构造新的类型。它的声明形式如下:
/** * From T, pick a set of properties whose keys are in the union K */ type Pick<T, K extends keyof T> = { [P in K]: T[P]; }; 复制代码
使用示例如下:
interface IPerson { name: string; age: number; height: number; } const person: Pick<IPerson, "name" | "age"> = { name: "zhangsan", age: 18 } 复制代码
5. Record<K extends keyof any, T>
Record 用来构造一个类型,其属性名的类型为K,属性值的类型为T。这个工具泛型可用来将某个类型的属性映射到另一个类型上,下面是其声明形式:
/** * Construct a type with a set of properties K of type T */ type Record<K extends keyof any, T> = { [P in K]: T; }; 复制代码
使用示例如下:
interface IPageinfo { title: string; } type IPage = 'home' | 'about' | 'contact'; const page: Record<IPage, IPageinfo> = { about: {title: 'about'}, contact: {title: 'contact'}, home: {title: 'home'}, } 复制代码
6. Exclude<T, U>
Exclude 就是从一个联合类型中排除掉属于另一个联合类型的子集,下面是其声明的形式:
/** * Exclude from T those types that are assignable to U */ type Exclude<T, U> = T extends U ? never : T; 复制代码
使用示例如下:
interface IPerson { name: string; age: number; height: number; } const person: Exclude<IPerson, "age" | "sex"> = { name: "zhangsan"; height: 180; } 复制代码
7. Omit<T, K extends keyof any>
上面的Pick 和 Exclude 都是最基础基础的工具泛型,很多时候用 Pick 或者 Exclude 还不如直接写类型更直接。而 Omit 就基于这两个来做的一个更抽象的封装,它允许从一个对象中剔除若干个属性,剩下的就是需要的新类型。下面是它的声明形式:
/** * Construct a type with the properties of T except for those in type K. */ type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>; 复制代码
使用示例如下:
interface IPerson { name: string; age: number; height: number; } const person: Omit<IPerson, "age" | "height"> = { name: "zhangsan"; } 复制代码
8. ReturnType
ReturnType会返回函数返回值的类型,其声明形式如下:
/** * Obtain the return type of a function type */ type ReturnType<T extends (...args: any) => any> = T extends (...args: any) => infer R ? R : any; 复制代码
使用示例如下:
function foo(type): boolean { return type === 0 } type FooType = ReturnType<typeof foo> 复制代码
这里使用 typeof 是为了获取 foo 的函数签名,等价于 (type: any) => boolean。
七、Axios 封装
在React项目中,我们经常使用Axios库进行数据请求,Axios 是基于 Promise 的 HTTP 库,可以在浏览器和 node.js 中使用。Axios 具备以下特性:
- 从浏览器中创建 XMLHttpRequests;
- 从 node.js 创建 HTTP 请求;
- 支持 Promise API;
- 拦截请求和响应;
- 转换请求数据和响应数据;
- 取消请求;
- 自动转换 JSON 数据;
- 客户端支持防御 XSRF。
Axios的基本使用就不再多介绍了。为了更好地调用,做一些全局的拦截,通常会对Axios进行封装,下面就使用TypeScript对Axios进行简单封装,使其同时能够有很好的类型支持。Axios是自带声明文件的,所以我们无需额外的操作。
下面来看基本的封装:
import axios, { AxiosInstance, AxiosRequestConfig, AxiosPromise,AxiosResponse } from 'axios'; // 引入axios和定义在node_modules/axios/index.ts文件里的类型声明 // 定义接口请求类,用于创建axios请求实例 class HttpRequest { // 接收接口请求的基本路径 constructor(public baseUrl: string) { this.baseUrl = baseUrl; } // 调用接口时调用实例的这个方法,返回AxiosPromise public request(options: AxiosRequestConfig): AxiosPromise { // 创建axios实例,它是函数,同时这个函数包含多个属性 const instance: AxiosInstance = axios.create() // 合并基础路径和每个接口单独传入的配置,比如url、参数等 options = this.mergeConfig(options) // 调用interceptors方法使拦截器生效 this.interceptors(instance, options.url) // 返回AxiosPromise return instance(options) } // 用于添加全局请求和响应拦截 private interceptors(instance: AxiosInstance, url?: string) { // 请求和响应拦截 } // 用于合并基础路径配置和接口单独配置 private mergeConfig(options: AxiosRequestConfig): AxiosRequestConfig { return Object.assign({ baseURL: this.baseUrl }, options); } } export default HttpRequest; 复制代码
通常baseUrl在开发环境的和生产环境的路径是不一样的,所以可以根据当前是开发环境还是生产环境做判断,应用不同的基础路径。这里要写在一个配置文件里:
export default { api: { devApiBaseUrl: '/test/api/xxx', proApiBaseUrl: '/api/xxx', }, }; 复制代码
在上面的文件中引入这个配置:
import { api: { devApiBaseUrl, proApiBaseUrl } } from '@/config'; const apiBaseUrl = env.NODE_ENV === 'production' ? proApiBaseUrl : devApiBaseUrl; 复制代码
之后就可以将apiBaseUrl作为默认值传入HttpRequest的参数:
class HttpRequest { constructor(public baseUrl: string = apiBaseUrl) { this.baseUrl = baseUrl; } 复制代码
接下来可以完善一下拦截器类,在类中interceptors方法内添加请求拦截器和响应拦截器,实现对所有接口请求的统一处理:
private interceptors(instance: AxiosInstance, url?: string) { // 请求拦截 instance.interceptors.request.use((config: AxiosRequestConfig) => { // 接口请求的所有配置,可以在axios.defaults修改配置 return config }, (error) => { return Promise.reject(error) }) // 响应拦截 instance.interceptors.response.use((res: AxiosResponse) => { const { data } = res const { code, msg } = data if (code !== 0) { console.error(msg) } return res }, (error) => { return Promise.reject(error) }) } 复制代码
到这里封装的就差不多了,一般服务端会将状态码、提示信息和数据封装在一起,然后作为数据返回,所以所有请求返回的数据格式都是一样的,所以就可以定义一个接口来指定返回的数据结构,可以定义一个接口:
export interface ResponseData { code: number data?: any msg: string } 复制代码
接下来看看使用TypeScript封装的Axios该如何使用。可以先定义一个请求实例:
import HttpRequest from '@/utils/axios' export * from '@/utils/axios' export default new HttpRequest() 复制代码
这里把请求类导入进来,默认导出这个类的实例。之后创建一个登陆接口请求方法:
import axios, { ResponseData } from './index' import { AxiosPromise } from 'axios' interface ILogin { user: string; password: number | string } export const loginReq = (data: ILogin): AxiosPromise<ResponseData> => { return axios.request({ url: '/api/user/login', data, method: 'POST' }) } 复制代码
这里封装登录请求方法loginReq,他的参数必须是我们定义的ILogin接口的类型。这个方法返回一个类型为AxiosPromise
的Promise,AxiosPromise是axios声明文件内置的类型,可以传入一个泛型变量参数,用于指定返回的结果中data字段的类型。
接下来可以调用一下这个登录的接口:
import { loginReq } from '@/api/user' const Home: FC = () => { const login = (params) => { loginReq(params).then((res) => { console.log(res.data.code) }) } } 复制代码
通过这种方式,当我们调用loginReq接口时,就会提示我们,参数的类型是ILogin,需要传入几个参数。这样编写代码的体验就会好很多。
八. 其他
1. import React
在React项目中使用TypeScript时,普通组件文件后缀为.tsx,公共方法文件后缀为.ts。在. tsx 文件中导入 React 的方式如下:
import * as React from 'react' import * as ReactDOM from 'react-dom' 复制代码
这是一种面向未来的导入方式,如果想在项目中使用以下导入方式:
import React from "react"; import ReactDOM from "react-dom"; 复制代码
就需要在tsconfig.json配置文件中进行如下配置:
"compilerOptions": { // 允许默认从没有默认导出的模块导入。 "allowSyntheticDefaultImports": true, } 复制代码
2. Types or Interfaces?
我们可以使用types或者Interfaces来定义类型吗,那么该如何选择他俩呢?建议如下:
- 在定义公共 API 时(比如编辑一个库)使用 interface,这样可以方便使用者继承接口,这样允许使用最通过声明合并来扩展它们;
- 在定义组件属性(Props)和状态(State)时,建议使用 type,因为 type 的约束性更强。
interface 和 type 在 ts 中是两个不同的概念,但在 React 大部分使用的 case 中,interface 和 type 可以达到相同的功能效果,type 和 interface 最大的区别是:type 类型不能二次编辑,而 interface 可以随时扩展:
interface Animal { name: string } // 可以继续在原属性基础上,添加新属性:color interface Animal { color: string } type Animal = { name: string } // type类型不支持属性扩展 // Error: Duplicate identifier 'Animal' type Animal = { color: string } 复制代码
type对于联合类型是很有用的,比如:type Type = TypeA | TypeB。而interface更适合声明字典类行,然后定义或者扩展它。
3. 懒加载类型
如果我们想在React router中使用懒加载,React也为我们提供了懒加载方法的类型,来看下面的例子:
export interface RouteType { pathname: string; component: LazyExoticComponent<any>; exact: boolean; title?: string; icon?: string; children?: RouteType[]; } export const AppRoutes: RouteType[] = [ { pathname: '/login', component: lazy(() => import('../views/Login/Login')), exact: true }, { pathname: '/404', component: lazy(() => import('../views/404/404')), exact: true, }, { pathname: '/', exact: false, component: lazy(() => import('../views/Admin/Admin')) } ] 复制代码
下面是懒加载类型和lazy方法在声明文件中的定义:
type LazyExoticComponent<T extends ComponentType<any>> = ExoticComponent<ComponentPropsWithRef<T>> & { readonly _result: T; }; function lazy<T extends ComponentType<any>>( factory: () => Promise<{ default: T }> ): LazyExoticComponent<T>; 复制代码
4. 类型断言
类型断言(Type Assertion)可以用来手动指定一个值的类型。在React项目中,断言还是很有用的,。有时候推断出来的类型并不是真正的类型,很多时候我们可能会比TS更懂我们的代码,所以可以使用断言(使用as关键字)来定义一个值得类型。
来看下面的例子:
const getLength = (target: string | number): number => { if (target.length) { // error 类型"string | number"上不存在属性"length" return target.length; // error 类型"number"上不存在属性"length" } else { return target.toString().length; } }; 复制代码
当TypeScript不确定一个联合类型的变量到底是哪个类型时,就只能访问此联合类型的所有类型里共有的属性或方法,所以现在加了对参数target和返回值的类型定义之后就会报错。这时就可以使用断言,将target的类型断言成string类型:
const getStrLength = (target: string | number): number => { if ((target as string).length) { return (target as string).length; } else { return target.toString().length; } }; 复制代码
需要注意,类型断言并不是类型转换,断言成一个联合类型中不存在的类型是不允许的。
再来看一个例子,在调用一个方法时传入参数:
这里就提示我们这个参数可能是undefined,而通过业务知道这个值是一定存在的,所以就可以将它断言成数字:data?.subjectId as number
除此之外,上面所说的标签类型、组件类型、时间类型都可以使用断言来指定给一些数据,还是要根据实际的业务场景来使用。
感悟:使用类型断言真的能解决项目中的很多报错~
5. 枚举类型
枚举类型在项目中的作用也是不可忽视的,使用枚举类型可以让代码的扩展性更好,当我想更改某属性值时,无需去全局更改这个属性,只要更改枚举中的值即可。通常情况下,最好新建一个文件专门来定义枚举值,便于引用。