Open zhangyongjian986 opened 3 years ago
最近在使用ts开发vue应用,在开发过程中遇到泛型这个概念。基于对于泛型的理解和认识,突发奇想,如果能够利用泛型的特点, 实现一个api自动提示的功能多好,这样不但对同一个项目中的其他开发者起到提示作用,省去查看文档的功夫;还可以把这一套方法放到我们公司的typescript项目模板中,方便其他同事开发使用,提高公司的研发效率。说干就干,下面就讲讲咋做的。 首先我们得了解几个概念
我们先看一段代码
class Stack { private data = []; pop () { return this.data.pop() } push (item) { this.data.push(item) } } const stack = new Stack(); stack.push(1); stack.push('string'); stack.pop();
上面是一个先进后出的栈的javascript实现,调用时,数据可以是任意类型。但当我们用typescript实现时,就应该传入指定类型,实现如下:
class Stack { private data:number = []; pop (): number { return this.data.pop(); } push (item: number) { this.data.push(item); } } const stack = new Stack(); stack.push(1); stack.push('string'); // Error: type error
上面的代码中,我们指定了栈中入栈和出栈的元素为number类型,如果我们入栈了非number类型,typescript编译器则会报错。但是我们实现的一个类往往并不止一个地方使用,里面涉及到的元素类型也可能是不一样的。那这个时候我们怎么样才能在不同地方调用这个Stack类时,里面的data元素可以是想要的类型呢。看下面代码
class Stack<T> { private data:T = []; pop (): T { return this.data.pop(); } push (item: T) { this.data.push(item); } } const stack = new Stack<string>(); stack.push(1); // Error: type error
上面代码中,我们给类Stack加了一个尖括号,并往里面传了一个T。这个T就是一个泛型,它表明我们这个类在调用时是可以传入不同的类型的。类似于函数可以传参一样,泛型中的T就是函数中的参数,可以认为是一个类型变量。这样,泛型给了我们传递类型的机会。
泛型分为接口泛型、类泛型,函数泛型。上面提到的是类泛型,是在定义类的时候,对于相关的值不特别指定类型,而是使用泛型类,以便在使用时传入特定类型,从而灵活其类型定义。比如typescript中我们常见的Promise类就是典型的类泛型。其在使用时必须传入一个类型以指定promise回调中的value值的类型。泛型函数则是实现了泛型接口的函数。我们看下面的代码
function getDataFromUrl<T>(url: sting): Promise<T> { return new Promise((resolve) => { setTimeout(() => { resolve(data); // }); }); }
此代码中,我们模拟实现了一个传入url去获取数据的方法,该方法返回了一个promise,promise的resolve值则是通过泛型T来指定。上面这种写法在我们ajax请求中常常见到,因为异步请求回来的响应值里面的数据类型并不是一成不变的,而是根据不同的接口来变化的。
在刚刚泛型函数的代码中,我们传入了T这个泛型,使用时可以这样使用
泛型函数
T
getDataFromUrl<number>('/userInfo').then(res => { constole.log(res); })
此时我们限定了响应里面的数据类型是number,当然,我们还可以指定为string,array等其他任何符合ts标准的类型。如果我们要指定T的范围呢?那就要用到泛型约束了,比如
function getDataFromUrl<T extends keyof string|number>(url: sting): Promise<T> { return new Promise((resolve) => { setTimeout(() => { resolve(data); // }); }); }
我们通过extends 关键字将T的范围限定在string和number内,在调用时,T的类型值就只能是这两种,否则将会报错 了解以上概念后,我们就可以开始着手实现我们想要的自动提示功能了。首先来看一下,我们想要实现的功能长啥样
string
number
import api from '@/service'; private async getUserInfo () { const res = await api('getUserInfo', {uid: 'xxxxx'}) // 省略若干代码 }
我们要实现的是,在上面输入调用这个api方法的时候,能够自动提示getUserInfo这个接口名,同时能够对我们的参数做一个限制. 而咱们的service长这样: 目标明确,咱们可以往下走。
getUserInfo
我们定义一个接口包含想要的接口方法:
interface Service {getUserInfo: (arg:any) => any} const service: Service = {getUserInfo: (params) => {return get('/usrInfo', params)}}
上面代码已经实现了自动提示方法名,但是还不够,我们的目标不但要能自动提示方法名,还能提示对应方法要传的参数的类型。那我们先把不同方法的参数先定义好 这里我们起一个文件叫params.d.ts的参数类型声明文件,用来定义一个Params模块,里面包含不同的接口或者说方法对应的参数类型 params.d.ts;
export interface getUserInfoParams { name: string; uid: string; } export default interface Params { getUserInfo: getUserInfoParams }
好了,我们再起一个文件,叫service.d.ts,里面用来声明我们的服务类,服务类包含了相应的接口
import { AxiosPromise } from "axios"; import Params from './params'; import { AnyObj } from "COMMON/interface/common"; interface ResponseValue { code: number; data: any; msg?: string; } type ServiceType = (params: AnyObj) => Promise<ResponseValue>; export type Service = { getUserInfo: ServiceType }
这样我们就有了两个文件和两个大的模块(类型Service和Params),那我们怎么把service里面的方法和params中对应的方法的参数类型联系起来呢? 我们可以这样想,首先我们service里面的方法,即key要和params中的key一样,那可以这样定义service接口吗
interface Service extends Params {}
显然,这样可以让Service 接口具有和params中一样的key,但是,这样不但继承了params的key,也继承了key的类型。但是,我们要的仅仅是 params中的key,Service中key的类型是一个返回类型为promise的方法,这不符合我们原意 我们上面提到了typescript的泛型工具类,其中一个叫Record,这个的功能就是将类型T中的key的类型转化为其他类型,这个其他类型由用户指定。 在这里,我们可以利用这个特性,不但可以获取到对应的key,还能满足上面提到的,Service的key的类型是一个返回类型为promise的方法。 如下,我们可以这样实现
type Service = Record<keyof Params, ServiceType>
这里,我们将Params里面的key提取出来传递给Record,同时指定了key的类型为ServiceType,这样就实现了一个Service类型,其属性和Params一样,属性 类型是ServiceType。改变后长这样
import { AxiosPromise } from "axios"; import Params from './params'; import { AnyObj } from "COMMON/interface/common"; interface ResponseValue { code: number; data: any; msg?: string; } type ServiceType = (params: AnyObj) => Promise<ResponseValue>; export type Service = Record<keyof Params, ServiceType>
目前为止,Service接口已经和Params类型有了一定的联系了,即Service中出现的key一定要在Params中出现,否则类型检测将不通过, 这就保证了开发的时候,每次增加一个接口方法,必须先在Params中定义该方法的参数类型。
我们要想办法,在调用方法(即service的key)的时候,能够让方法的参数类型与方法名起到关联。其实要起到这个关联也有一个简单的方法,即是在定义servie里面属性的方法时,直接在参数上定义 对应方法的参数类型即可,但是这不符合我们使用泛型的目的。既然是用到泛型,那我们就想到泛型是有传参这样的特性。如果我们在调用service中的方法时,也能够将方法名对应的参数类型从Params中取出来,那是不是就能达到我们的目的了?我们先定义一个函数,可以传入我们的service中的key作为参数,并调用service中的方法,返回方法的返回值
const api = (method) {return service[method]()};
此方法在调用service时需要能传入参数,于是变成下面这样
const api = (method, parmas) {return service[method](params)};
根据我们上面的目的,将api函数的参数类型设置为泛型,而这个泛型参数我们需要约束为是Service类中的方法名。根据上面说过的,约束可以用extends关键词。 于是便有
const api<T extends keyof Service> = (method: T, params: Params[T]){return service[method](parmas)};
这样,我们就能够通过api调用service,并有方法名和参数类型提示。 到此,我们这个自动提示api方法及参数的小功能就实现了,在开发过程中,只要调用api方法,便会自动弹出可选的api方法。在复杂项目中,开发人员只要在params上定义好对应的接口及参数类型,在每次调用时便会有提示,免去不断翻看接口文档的烦恼,大大提高开发效率。当然,这个小功能还可以增加一个响应数据自动提示的功能,这里就不提及了,留给大家做一个思考。 完整的代码: params.d.ts
export interface getUserInfoParams {} export default interface Params { getUserInfo: getUserInfoParams }
service.d.ts:
import { AxiosPromise } from "axios"; import Params from './params'; import API from './api'; import { AnyObj } from "COMMON/interface/common"; interface ResponseValue { code: number; data: any; msg?: string; } type ServiceType = (params: AnyObj) => Promise<ResponseValue>; export type Service = Record<keyof Params, ServiceType>
service/index.ts:
import { get, post } from 'COMMON/xhr-axios'; import {Service} from './types/service'; import Params from './types/params'; const service:Service = { getUserInfo: (params) => { return get('/usrinfo', params); } } const api = <FN extends keyof Params>(fn: FN, params: Params[FN]) => { return service[fn](params) } // 用法 // import api from '@/service/index' // api('getUserInfo', {}) export default api;
使用:
private async getUserInfo () { const res = await api('getUserInfo', {uid: 'xxxxx'}) // 省略若干代码 }
如何使用泛型写一个自动提示api方法及参数的功能
最近在使用ts开发vue应用,在开发过程中遇到泛型这个概念。基于对于泛型的理解和认识,突发奇想,如果能够利用泛型的特点, 实现一个api自动提示的功能多好,这样不但对同一个项目中的其他开发者起到提示作用,省去查看文档的功夫;还可以把这一套方法放到我们公司的typescript项目模板中,方便其他同事开发使用,提高公司的研发效率。说干就干,下面就讲讲咋做的。 首先我们得了解几个概念
泛型
我们先看一段代码
上面是一个先进后出的栈的javascript实现,调用时,数据可以是任意类型。但当我们用typescript实现时,就应该传入指定类型,实现如下:
上面的代码中,我们指定了栈中入栈和出栈的元素为number类型,如果我们入栈了非number类型,typescript编译器则会报错。但是我们实现的一个类往往并不止一个地方使用,里面涉及到的元素类型也可能是不一样的。那这个时候我们怎么样才能在不同地方调用这个Stack类时,里面的data元素可以是想要的类型呢。看下面代码
上面代码中,我们给类Stack加了一个尖括号,并往里面传了一个T。这个T就是一个泛型,它表明我们这个类在调用时是可以传入不同的类型的。类似于函数可以传参一样,泛型中的T就是函数中的参数,可以认为是一个类型变量。这样,泛型给了我们传递类型的机会。
泛型函数
泛型分为接口泛型、类泛型,函数泛型。上面提到的是类泛型,是在定义类的时候,对于相关的值不特别指定类型,而是使用泛型类,以便在使用时传入特定类型,从而灵活其类型定义。比如typescript中我们常见的Promise类就是典型的类泛型。其在使用时必须传入一个类型以指定promise回调中的value值的类型。泛型函数则是实现了泛型接口的函数。我们看下面的代码
此代码中,我们模拟实现了一个传入url去获取数据的方法,该方法返回了一个promise,promise的resolve值则是通过泛型T来指定。上面这种写法在我们ajax请求中常常见到,因为异步请求回来的响应值里面的数据类型并不是一成不变的,而是根据不同的接口来变化的。
泛型约束
在刚刚
泛型函数
的代码中,我们传入了T
这个泛型,使用时可以这样使用此时我们限定了响应里面的数据类型是number,当然,我们还可以指定为string,array等其他任何符合ts标准的类型。如果我们要指定T的范围呢?那就要用到泛型约束了,比如
我们通过extends 关键字将T的范围限定在
string
和number
内,在调用时,T的类型值就只能是这两种,否则将会报错了解以上概念后,我们就可以开始着手实现我们想要的自动提示功能了。首先来看一下,我们想要实现的功能长啥样
我们要实现的是,在上面输入调用这个api方法的时候,能够自动提示
getUserInfo
这个接口名,同时能够对我们的参数做一个限制. 而咱们的service长这样: 目标明确,咱们可以往下走。第一步,怎么让serveice能够自动提示方法名或者接口名
我们定义一个接口包含想要的接口方法:
上面代码已经实现了自动提示方法名,但是还不够,我们的目标不但要能自动提示方法名,还能提示对应方法要传的参数的类型。那我们先把不同方法的参数先定义好 这里我们起一个文件叫params.d.ts的参数类型声明文件,用来定义一个Params模块,里面包含不同的接口或者说方法对应的参数类型 params.d.ts;
好了,我们再起一个文件,叫service.d.ts,里面用来声明我们的服务类,服务类包含了相应的接口
这样我们就有了两个文件和两个大的模块(类型Service和Params),那我们怎么把service里面的方法和params中对应的方法的参数类型联系起来呢? 我们可以这样想,首先我们service里面的方法,即key要和params中的key一样,那可以这样定义service接口吗
显然,这样可以让Service 接口具有和params中一样的key,但是,这样不但继承了params的key,也继承了key的类型。但是,我们要的仅仅是 params中的key,Service中key的类型是一个返回类型为promise的方法,这不符合我们原意 我们上面提到了typescript的泛型工具类,其中一个叫Record,这个的功能就是将类型T中的key的类型转化为其他类型,这个其他类型由用户指定。 在这里,我们可以利用这个特性,不但可以获取到对应的key,还能满足上面提到的,Service的key的类型是一个返回类型为promise的方法。 如下,我们可以这样实现
这里,我们将Params里面的key提取出来传递给Record,同时指定了key的类型为ServiceType,这样就实现了一个Service类型,其属性和Params一样,属性 类型是ServiceType。改变后长这样
目前为止,Service接口已经和Params类型有了一定的联系了,即Service中出现的key一定要在Params中出现,否则类型检测将不通过, 这就保证了开发的时候,每次增加一个接口方法,必须先在Params中定义该方法的参数类型。
第二步,在调用service的方法时自动提示接口的参数类型
我们要想办法,在调用方法(即service的key)的时候,能够让方法的参数类型与方法名起到关联。其实要起到这个关联也有一个简单的方法,即是在定义servie里面属性的方法时,直接在参数上定义 对应方法的参数类型即可,但是这不符合我们使用泛型的目的。既然是用到泛型,那我们就想到泛型是有传参这样的特性。如果我们在调用service中的方法时,也能够将方法名对应的参数类型从Params中取出来,那是不是就能达到我们的目的了?我们先定义一个函数,可以传入我们的service中的key作为参数,并调用service中的方法,返回方法的返回值
此方法在调用service时需要能传入参数,于是变成下面这样
根据我们上面的目的,将api函数的参数类型设置为泛型,而这个泛型参数我们需要约束为是Service类中的方法名。根据上面说过的,约束可以用extends关键词。 于是便有
这样,我们就能够通过api调用service,并有方法名和参数类型提示。
到此,我们这个自动提示api方法及参数的小功能就实现了,在开发过程中,只要调用api方法,便会自动弹出可选的api方法。在复杂项目中,开发人员只要在params上定义好对应的接口及参数类型,在每次调用时便会有提示,免去不断翻看接口文档的烦恼,大大提高开发效率。当然,这个小功能还可以增加一个响应数据自动提示的功能,这里就不提及了,留给大家做一个思考。
完整的代码: params.d.ts
service.d.ts:
service/index.ts:
使用: