bigo-frontend / blog

👨🏻‍💻👩🏻‍💻 bigo前端技术博客
https://juejin.cn/user/4450420286057022/posts
MIT License
129 stars 9 forks source link

如何使用泛型写一个自动提示api方法及参数的功能 #52

Open zhangyongjian986 opened 3 years ago

zhangyongjian986 commented 3 years ago

如何使用泛型写一个自动提示api方法及参数的功能

最近在使用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这个泛型,使用时可以这样使用

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的范围限定在stringnumber内,在调用时,T的类型值就只能是这两种,否则将会报错
了解以上概念后,我们就可以开始着手实现我们想要的自动提示功能了。首先来看一下,我们想要实现的功能长啥样

import api from '@/service';
private async getUserInfo () {
  const res = await api('getUserInfo', {uid: 'xxxxx'})
  // 省略若干代码
}

我们要实现的是,在上面输入调用这个api方法的时候,能够自动提示getUserInfo这个接口名,同时能够对我们的参数做一个限制. 而咱们的service长这样: 目标明确,咱们可以往下走。

第一步,怎么让serveice能够自动提示方法名或者接口名

我们定义一个接口包含想要的接口方法:

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的方法时自动提示接口的参数类型

我们要想办法,在调用方法(即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'})
  // 省略若干代码
}