In your day job, have you ever read a Typescript document a dozen times and then struggled to write the actual Typescript code, encountering an error and writing any after a search failed? 🤷♀️ (I guess so, otherwise you wouldn’t have clicked on this article. 👻

The thing that keeps you from going one step further is that generics are not fully mastered. This article will start with an example that I come across in my daily work. Step by step, I will introduce where generics are needed and how to write ~

(What if you’re not familiar with Typescript beyond generics 😰? Learn about Typescript with examples in a more comprehensive article I compiled earlier.

Let ‘s begin.

The problem

The backend provides multiple interfaces to support paging lookup of list data, and these interfaces may have different parameter formats, response results, and paging forms. In the case of paging, there are several common types of paging parameters: number of pages and number of pages per page, passing offsets and limits, using the last ID of the previous page to query, and so on.

{
  page_size: number,
  page_num: number
}

{
  offset: number,
  limit: number
}

{
  forward: boolean
  last_id: string
  page_size: number
}

...
Copy the code

The data volume of these interfaces is about thousands of pieces of data. Considering the pressure of the database, backend students are not recommended to pull thousands of pieces of data at a time. They need to pull all the data in front pages.

In order to avoid paging logic every interface write once, requires the implementation of a strongly typed tool method, to achieve automatic paging pull all data function.

Code implementation

The focus of this article is not how to achieve such a function, a simple flowchart, I believe that most people can achieve.

A viable code implementation is as follows:

const unpaginate = (api, config,) = > {
  const { getParams, hasMore, dataAdaptor } = config

  async function iterator(time, lastRes) {
    // Get the parameters of the next request from the result of the last request and the number of requests
    const params = getParams(lastRes, time)
    const res = await api(params)

    let next = []

    // If there is a next page, continue to pull
    if (hasMore(res, params)) {
      next = await iterator(time + 1, res)
    }

    // Return the result together
    return dataAdaptor(res).concat(next)
  }

  return iterator()
}
Copy the code

The first argument to the unpaginate method passes in an API method that returns a Promise result; The second argument supports passing in a configurable object:

The getParams method returns the result of the last request and the current number of requests, so that users can set the request parameters. The hasMore method returns the results and parameters of the current request, requiring the user to tell the program whether it has finished pulling. The dataAdaptor method sends back the result of each request, allowing you to customize the format of the return result (for example, changing the underscore of a field to a hump), and saving the return value as the final result;

Do you implement typing in Typescript? Are you type safe? Will there be code prompts when coding? Is it any shuttle?

Next, we will provide type support for this method step by step.

Typescritp generic support

Start with parameters and write the most basic type declarations for the API and Config.

export interface Config {
  hasMore: (res? : any, params? : any) = > boolean
  getParams: (res? : any, time? : number) = > any
  dataAdaptor: (res: any) = > any[]
}

const unpaginate = (
  api: (params: any) = > Promise<any[]>,
  config: Config,
): Promise<any[]> => {
  ...
}
Copy the code

The above type declaration doesn’t do much (because any is everywhere), but it’s better than nothing, at least when passing parameters to API and Config that don’t match the type.

The first generics — parameter types

It is easy to see that the parameters of the method in the Config type are strongly related to the API type. The type of the API parameter determines the params parameter type of the hasMore method. The type of return result is used by all three methods.

Speaking of methods, in Typescript, you can use Parameters, ReturnType to extract parameter types and return value types from method types.

type EventListenerParamsType = Parameters<typeof window.addEventListener>;
// [type: string, listener: EventListenerOrEventListenerObject, options?: boolean | AddEventListenerOptions | undefined]
	
type A = (a: number) = > string
type B = ReturnType<A>
// string
Copy the code

Here the API is not a fixed type, you need to extract the type from the dynamic API type, generics come into play.

const unpaginate = <T extends (params: any) => Promise<any>>(
  api: T,
  config: Config,
): Promise<any[]> => {
  ...
}
Copy the code

We prefix the method with

Promise

> to declare a generic type. The extends limits the generics: it must be a method and return a Promise result.

We then assign type T to the API, and when we use type T after we write it, Typescript automatically deduces dynamically from the actual API method type called.

The API is generic, and Config needs to be generic, too. Generics are passed as parameters.

export interface Config<P> {
  hasMore: (res? : R, params? : P) = > boolean
  / /...
}
Copy the code

Interface Config

Here we make Config support generic parameters as well, passing it to the parmas parameter. You can think of P as just an arbitrary variable name, or you can change it to T.

In conjunction with the Parameters generic utility method, take the first parameter type of T and pass it to Config so that their types are related.


const unpaginate = <T extends (params: any) => Promise<any>>(
  api: T,
  config: Config<Parameters<T>[0]>,
): Promise<any[]> => {
  ...
}
Copy the code

Parameters

[0] means that you take the first parameter type of a parameter of type T (which is an array type).

The second generic — the type of the return value

The parameter types can be derived dynamically, and the API’s return result can be implemented using the same operation.

One tricky problem is that the API returns a result of type Promsie

, while config returns a result of type R that should be removed from the promise-made one.

To extract types from generics, we’ll use infer. Go straight to the code:

type UnPromise<T> = T extends Promise<infer U> ? U : undefined

type A = Promise<number>
type B = UnPromise<A>
// number
Copy the code

If generics are dynamic types, infer is dynamic types. In the example above, we use the extends clause to tell Typescript that the type needs to be derived dynamically.

Extract the entity type of the return value and continue to refine the type definition:

export interface Config<P, R> {
  hasMore: (res? : R, params? : P) = > boolean

  getParams: (res? : R, time? : number) = > Partial<P>

  dataAdaptor: (res: R) = > any[]
}

type UnPromise<T> = T extends Promise<infer U> ? U : undefined

const unpaginate = <
  T extends (params: any) => Promise<any>,
  U extends UnPromise<ReturnType<T>>
>(
  api: T,
  config: Config<Parameters<T>[0], U>,
): Promise<any[]> => {
  ...
}
Copy the code

The second generic U is dynamically derived from UnPromise

> and then passed to Config to complete the type conduction of the returned result.

The third generic — the formatted result type

The last remaining issue to deal with is the result type of the return value of dataAdaptor. There is no limit to what it can return, just let Typescirpt derive and pass on its own. And as the return type of the unpaginate method.

Here we need to define another generic:

export interface Config<P, R, V> {
  / /...
  dataAdaptor: (res: R) = > V[]
}

const unpaginate = <
  T extends (params: any) => Promise<any>,
  U extends UnPromise<ReturnType<T>>,
  V extends any
>(
  api: T,
  config: Config<Parameters<T>[0], U, V>,
): Promise<V[]>
Copy the code

We use V extends any to define a new generic type and pass it to the result returned by config. dataAdaptor, dataAdaptor: (res: R) => V[] so that Typescript can deduce the type of V from the array type returned by dataAdaptor => in a specific scenario.

Use V[] as the return type of unpaginate, so you can string everything together.

The final result

API method parameter derivation:

The API method returns the result derived:

Return result derivation after formatting:

It can be played on Typescript Playground, and the code can be found on my Github.

Ending

Hopefully, this article will help you with a step-by-step guide to using generics to implement type declarations for a common method. For those of you who are not familiar with Typescript, I wrote another article on Learning Typescript by Example.