This article was originally published at: github.com/bigo-fronte… Welcome to follow and reprint.

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

I recently came across the concept of generics while developing VUE applications using TS. Based on the understanding and understanding of generics, it occurred to me that it would be nice to use the characteristics of generics to implement an API auto-prompt function, which would not only prompt other developers in the same project, but also save the effort of checking documents. We can also put this set of methods into our company’s typescript project templates for other colleagues to develop and use, and improve the company’s r&d efficiency. Just do it. Here’s how. First we need to understand a couple of concepts

The generic

So let’s look at a piece of code

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();
Copy the code

The above is a javascript implementation of a first-in-last-out stack. When called, the data can be of any type. But when we implement typescript, we pass in the specified type, which looks like this:

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
Copy the code

In the above code, we specify that the elements on and off the stack are of type number. If we push a non-number type, the typescript compiler will report an error. However, we often implement a class that is not used in more than one place, and the element types involved may be different. So how can we call this Stack class in different places, so that the data element in it can be the type that we want. Look at the following code

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
Copy the code

In the code above, we add Angle brackets to the Stack class and pass a T inside. The T is a generic, indicating that our class can pass in different types when called. Just as a function can pass arguments, T in a generic is a parameter in a function and can be considered a type variable. In this way, generics give us the opportunity to pass in types.

Generic function

Generics are divided into interface generics, class generics, and function generics. The above mentioned is class generics, which means that when defining a class, you do not specify a specific type for the associated value. Instead, you use a generic class so that you can pass in a specific type when you use it, thereby making your type definition flexible. For example, the common Promise class in typescript is a classic class generics. It must be used with a type that specifies the type of the value in the Promise callback. A generic function is one that implements a generic interface. Let’s look at the code below

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

In this code, we mock the implementation of a method that passes in a URL to retrieve data, returning a promise whose resolve value is specified by the generic T. This type of writing is common in Ajax requests, because the data type in the response value returned by an asynchronous request is not immutable, but varies from interface to interface.

Generic constraint

In the generic function code, we passed the T generic, which we can use like this

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

At this point we restrict the data type in the response to number, and of course we can specify string, array, and any other type that meets the TS standard. What if we wanted to specify the range of T? That’s where generic constraints come in, for example

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

We use the extends keyword to limit T to string and number. When called, T can only be of either type or we’ll get an error. With that in mind, we’re ready to implement the auto-prompt we want. First, let’s look at what the functionality we want to implement looks like

import api from '@/service';
private async getUserInfo () {
  const res = await api('getUserInfo', {uid: 'xxxxx'})
  // Omit some code
}
Copy the code

What we want to do is to automatically prompt getUserInfo for the interface name when we call the API method above, and to put a limit on our parameters. Our service looks like this: With a clear goal, we can go down.

Step 1: How do I get serveice to automatically prompt method or interface names

We define an interface containing the desired interface methods:

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

The above code already does this, but it’s not enough. Our goal is not only to automatically prompt for a method name, but also for the type of argument to be passed by the corresponding method. To define the parameters of different methods, we use a file called params.d.ts parameter type declaration file to define a params module, which contains different interfaces or methods corresponding parameter types params.d.ts;

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

export default interface Params {
    getUserInfo: getUserInfoParams
}
Copy the code

Ok, we have a new file called service.d.ts that declares 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
}
Copy the code

So we have two files and two large modules (Service and Params). How do we relate the methods in Service to the parameter types in Params? We can think of it this way: first of all, the method in our service, the key, is the same as the key in params. Can we define the service interface like this

interface Service extends Params {}
Copy the code

Obviously, this allows the Service interface to have the same key as in Params, but it inherits both the params key and the type of the key. However, all we need is the key in Params, and the key in Service is a method that returns a promise. That’s not what we want. We mentioned typescript’s generic utility classes, one of which is called Record, The function of this is to convert the key of type T to another type specified by the user. In this case, we can use this feature not only to obtain the corresponding key, but also to satisfy the above mentioned requirement that the Service’s key type is a method that returns a promise type. As follows, we can do this

type Service = Record<keyof Params, ServiceType>
Copy the code

Here, we extract the key from Params and pass it to Record, specifying that the key is of type ServiceType. This implements a Service type with the same attribute type ServiceType as Params. 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>
Copy the code

So far, the Service interface has been associated with the Params type. That is, the key that appears in the Service must appear in the Params, otherwise the type detection will fail. This ensures that every time you add an interface method, you must first define the parameter type of that method in Params.

In the second step, the interface parameter type is automatically prompted when the method of the Service is called

We need to find a way to associate the method’s parameter type with the method name when calling the method (i.e. the service key). There is an easy way to do this, which is to define the parameter type of the method in servie, but this does not meet the purpose of using generics. Since we are using generics, we should think of generics as having the property of passing parameters. If we could also pull the parameter type of the method name out of Params when calling a method in service, wouldn’t that accomplish our purpose? We define a function that takes the key of our service as an argument and calls the method in the service, returning the return value of the method

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

This method needs to be able to pass in arguments when calling the service, so it looks like this

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

For our purposes above, we set the parameter type of the API function to generic, and the generic parameter we need to constrain is the method name in the Service class. As mentioned above, constraints can use the extends keyword. So we have

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

In this way, we can call the Service through the API with a method name and parameter type hint. At this point, our automatic prompt API method and parameters of the small function is realized, in the development process, as long as the API method is called, it will automatically pop up the optional API method. In complex projects, as long as the developer defines the corresponding interface and parameter type on Params, there will be a prompt every time it is called, which saves the trouble of constantly looking through the interface document and greatly improves the development efficiency. Of course, this small function can also add a response to the data automatically prompt function, which is not mentioned here, I leave you to think about. The complete code: params.d.ts

export interface getUserInfoParams {}

export default interface Params {
    getUserInfo: getUserInfoParams
}
Copy the code

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>
Copy the code

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;Copy the code

Use:

private async getUserInfo () {
  const res = await api('getUserInfo', {uid: 'xxxxx'})
  // Omit some code
}
Copy the code

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

I’m bigO front. See you next time.