The parameter and response data types of the TypeScript Web project API are missing by default without manual mapping:

async function sendRequest(url: string, params? :any) {
  const response = axios.get(url, { params })
  return response  // -> Promise<AxiosResponse<any, any>> 
}
Copy the code

This brings a little instability to the project. If it’s complicated, the response data for each interface is any, and the various interfaces/return data depend on each other to reflect the confusion.

This is done by writing a generic request function, sendRequest (sample jump in action) :

Specifying the response type

If you look at the type of axios, you can see that it supports specifying interface response types:

export class Axios {
    get<T = any, R = AxiosResponse<T>, D = any>(url: string, config? : AxiosRequestConfig<D>):Promise<R>;
}
Copy the code

To do this, specify the generic T parameter to let TS derive the response data type, modifying the initial code:

// Assume interface A's path is '/apple' and the response type is AppleRes

interface AppleRes {
  code: number
  data: string
}

async function sendRequest<T> (url: string, params? :any) {
  const response = axios.get<T>(url, { params })
  return response
}

const apple = sendRequest<AppleRes>('/apple') // -> Promise<AxiosResponse<AppleRes, any>>

apple.then((res) = > {
  const blah = res.data.data // -> string
  
  const blah2 = res.data.data2 // Error: Property 'data2' does not exist on type 'AppleRes'. Did you mean 'data'?
})
Copy the code

At this point, TS can deduce the response type, and when we enter a nonexistent attribute, TS indicates that the attribute does not exist.

After all, we can also write the as AppleRes mapping type in the request. Continue below.

Specify parameter types

The mapping parameter type is simple and only needs to be specified in the params parameter:

// Assume interface A's path is '/apple', the parameter type is AppleReq, and the response type is AppleRes

interface AppleReq {
  pageNum: numberpageSize? :number
}

async function sendRequest<T.R> (url: string, params? : R) {
  const response = axios.get<T>(url, { params })
  return response
}

const apple = sendRequest<AppleRes, AppleReq>('/apple', {
	pageNum: 1.// -> number 
	blah: 1 // Error: Argument of type '{ pageNum: number; blah: number; }' is not assignable to parameter of type 'AppleReq'.
})
Copy the code

This way, if we enter a wrong parameter, TS can correct it.

But it doesn’t seem to be enough. In this case, each request interface needs to manually enter the type of Req, Res, which is very troublesome.

Is there a way to input a sendRequest(‘/apple’) request path so that TS can deduce the type of request and response data?

Bind the request path & parameter & response data types

Suppose we have a number of interfaces, and we define their mapping, using interface is appropriate:

interface AppleRes {
  code: number
  data: string
}
interface AppleReq {
  pageNum: number
}

interface BananaRes {
  code: number
  data: object
}
interface BananaReq {
  pageSize: number
}

/ /...

// Key: bind their mappings in ApiMaps
interface ApiMaps {
  '/apple': { req: AppleReq; res: AppleRes }
  '/banana': { req: BananaReq; res: BananaRes }
  '/cat': { req: CatReq; res: CatRes }
}
Copy the code

Many enterprises have internal interface management platform, YY is the Tagee platform. Community version is also available, such as Swagger, Rap2, etc. The above parts can be easily generated in batches through the interface management platform.

This way we can get the request and response types of the path by using the ‘/apple’ key:

type AppleApiMap = ApiMaps['/apple']

// This is equivalent to:

type AppleApiMap = {
    req: AppleReq;
    res: AppleRes;
}
Copy the code

Then, we map in sendRequest:

// Get the set of types for the request path:
type ApiKeys = keyof ApiMaps 

async function sendRequest<T extends ApiKeys = ApiKeys> (url: T, params? : ApiMaps[T]['req']) {
  const response = await axios.get<ApiMaps[T]['res']>(url, { params })
  return response
}
Copy the code

Note: T extends ApiKeys = ApiKeys means that the above generic T is one of the collections of ApiKeys, i.e. ‘/apple’, ‘/banana’, and ‘/cat’.

= ApiKeys is the generic default. If we pass no generic parameters, TS can use the actual type of the parameter as the default type. See: TypeScript: Documentation – TypeScript 2.3 (typescriptlang.org)

The actual effect

const apple = sendRequest('/apple', { pageNum: 1 })
apple.then((res) = > {
  const blah = res.data.data // -> string
  const blah2 = res.data.data2 // Error: Property 'data2' does not exist on type 'AppleRes'. Did you mean 'data'?
})

const banana = sendRequest('/banana', { pageSize: 1 })
banana.then((res) = > {
  const blah = res.data.data // -> boolean
})
Copy the code

VSCode also automatically prompts you for a path of any type: