How to use generics to write a function that automatically prompts api methods and parameters

file

This article was first published at: https://github.com/bigo-frontend/blog/ Welcome to follow and repost.

How to use generics to write a function that automatically prompts api methods and parameters

Recently, I am using ts to develop Vue applications, and I encountered the concept of generics during the development process. Based on the understanding and understanding of generics, I had a whim, if it is possible to use the characteristics of generics
to implement an API automatic prompt function, it will not only remind other developers in the same project, but save the need to check Documentation effort; you can also put this set of methods into our company's typescript project template, which is convenient for other colleagues to develop and use, and improve the company's R&D efficiency. Let's do it, let's talk about how to do it.
First we have to understand a few concepts

generic

Let's look at a piece of code first

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();

The above is a javascript implementation of a first-in last-out stack. When calling, the data can be of any type. But when we implement it with typescript, we should pass in the specified type, as follows:

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

In the above code, we specify that the elements that are pushed and popped from the stack are of type number. If we push a non-number type into the stack, the typescript compiler will report an error. However, a class we implement is often used in more than one place, and the element types involved in it may also be different. Then how can we call this Stack class in different places at this time, so that the data elements inside can be of the desired type. Look at the code below

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

In the above code, we added angle brackets to the class Stack and passed a T into it. This T is a generic type, which indicates that our class can pass in different types when calling. Similar to how functions can pass parameters, T in generics is a parameter in the function, which can be considered as a type variable. In this way, generics give us the opportunity to pass types.

generic function

Generics are divided into interface generics, class generics, and function generics. The above-mentioned class generics means that when defining a class, the related value does not specify a specific type, but uses a generic class to pass in a specific type when using it, so as to flexibly define its type. For example, our common Promise class in typescript is a typical generic class. When it is used, a type must be passed in to specify the type of value in the promise callback. A generic function is a function that implements a generic interface. Let's look at the following code

function getDataFromUrl<T>(url: sting): Promise<T> {
    
    
  return new Promise((resolve) => {
    
    
    setTimeout(() => {
    
    
      resolve(data); //
    });
  });
}

In this code, we simulate a method of passing in a url to obtain data. This method returns a promise, and the resolve value of the promise is specified by the generic type T. The above writing method is often seen in our ajax request, because the data type in the response value returned by the asynchronous request is not static, but changes according to different interfaces.

Generic constraints

In 泛型函数the code just now, we passed in Tthis generic type, which can be used like this

getDataFromUrl<number>('/userInfo').then(res => {
    
    
  constole.log(res);
})

At this point, we limit the data type in the response to number. Of course, we can also specify it as string, array or any other type that meets the ts standard. What if we want to specify the range of T? Then use generic constraints, such as

function getDataFromUrl<T extends keyof string|number>(url: sting): Promise<T> {
    
    
  return new Promise((resolve) => {
    
    
    setTimeout(() => {
    
    
      resolve(data); //
    });
  });
}

We use the extends keyword to limit the scope of T to stringand numberwithin. When calling, the type value of T can only be these two types, otherwise an error will be reported.
After understanding the above concepts, we can start to realize the automatic function we want. Prompt function now. Let's first look at what the function we want to achieve looks like

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

What we want to achieve is that when we input and call this api method above, we can automatically prompt getUserInfothe interface name, and at the same time, we can set a limit on our parameters. And our service looks like this:
the goal is clear, we can go down.

The first step is how to enable serveice to automatically prompt the method name or interface name

We define an interface containing the desired interface methods:

interface Service {
    
    getUserInfo: (arg:any) => any}
const service: Service = {
    
    getUserInfo: (params) => {
    
    return get('/usrInfo', params)}}

The above code has realized the automatic prompt method name, but it is not enough. Our goal is not only to automatically prompt the method name, but also to prompt the type of the parameter to be passed by the corresponding method. Then we first define the parameters of different methods.
Here we create a parameter type declaration file called params.d.ts, which is used to define a Params module, which contains different interfaces or parameter types corresponding to methods
params.d .ts;

export interface getUserInfoParams {
    
    
  name: string;
  uid: string;
}

export default interface Params {
    
    
    getUserInfo: getUserInfoParams
}

Well, let's create another file called service.d.ts, which is used to declare our service class, which contains the corresponding interface

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
}

In this way, we have two files and two large modules (type Service and Params), so how do we link the method in service with the parameter type of the corresponding method in params?
We can think of it this way. First of all, the method in our service, that is, the key must be the same as the key in params. Can we define the service interface in this way?

interface Service extends Params {
    
    }

Obviously, this allows the Service interface to have the same key as in params, but this not only inherits the key of params, but also inherits the type of the key. However, what we want is only
the key in params. The type of key in Service is a method whose return type is promise, which does not meet our original intention. We
mentioned the generic tool class of typescript above, one of which is called Record, and the function of this It is to convert the type of the key in type T into another type, and this other type is specified by the user.
Here, we can take advantage of this feature, not only can obtain the corresponding key, but also satisfy the above-mentioned, the type of the key of Service is a method whose return type is promise.
As follows, we can achieve

type Service = Record<keyof Params, ServiceType>

Here, we extract the key in Params and pass it to Record, and at the same time specify the type of the key as ServiceType, so that a Service type is realized, its attributes are the same as Params, and the attribute type is ServiceType
. After changing it looks like this

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>

So far, the Service interface has a certain relationship with the Params type, that is, the key that appears in the Service must appear in the Params, otherwise the type detection will fail, which ensures that an interface method is added each time during
development , you must first define the parameter type of the method in Params.

The second step is to automatically prompt the parameter type of the interface when calling the service method

We have to find a way to associate the parameter type of the method with the method name when calling the method (that is, the key of the service).
In fact, there is a simple way to achieve this association, that is, when defining the method of the attribute in the service, directly define the parameter type of the corresponding method on the parameter , but this does not meet the purpose of our use of generics. Since generics are used, we think that generics have the feature of passing parameters. If we can also extract the parameter type corresponding to the method name from Params when calling the method in the service, will we be able to achieve our goal? We first define a function that can pass in the key in our service as a parameter, call the method in the service, and return the return value of the method

const api = (method) {
    
    return service[method]()};

This method needs to be able to pass in parameters when calling the service, so it becomes the following

const api = (method, parmas) {
    
    return service[method](params)};

According to our above purpose, set the parameter type of the api function to generic, and we need to constrain this generic parameter to be the method name in the Service class. According to what was said above, constraints can use the extends keyword.
So there is

const api<T extends keyof Service> = (method: T, params: Params[T]){
    
    return service[method](parmas)};

In this way, we can call the service through the API, and have method name and parameter type hints.
So far, our small function of automatically prompting the api method and parameters has been realized. During the development process, as long as the api method is called, the optional api method will automatically pop up. In complex projects, developers only need to define the corresponding interfaces and parameter types on params, and there will be prompts every time they are called, eliminating the trouble of constantly looking through interface documents and greatly improving development efficiency. Of course, this small function can also add a function of automatically prompting the response data, which will not be mentioned here, and I will leave it for everyone to think about.
Complete code:
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;

use:

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

Welcome everyone to leave a message to discuss, I wish you a smooth work and a happy life!

I am the front end of bigo, see you in the next issue.

Guess you like

Origin blog.csdn.net/yeyeye0525/article/details/121329384