Extending Interfaces
# Extending Interfaces
# Requirements Analysis
To make it more convenient for users to send requests with axios, we can extend some interfaces for all supported request methods:
axios.request(config)axios.get(url[, config])axios.delete(url[, config])axios.head(url[, config])axios.options(url[, config])axios.post(url[, data[, config]])axios.put(url[, data[, config]])axios.patch(url[, data[, config]])
When using these methods, we no longer need to specify the url, method, or data properties in the config object.
From a requirements perspective, axios is no longer just a function but more like a hybrid object -- it is a function itself and also has many method properties. Next, let's implement this hybrid object.
# Interface Type Definitions
Based on the requirements analysis, the hybrid object axios itself is a function. We will implement a class that includes its method properties, then copy the prototype properties and instance properties of this class onto axios.
Let's first define the interface for the axios hybrid object:
types/index.ts:
export interface Axios {
request(config: AxiosRequestConfig): AxiosPromise
get(url: string, config?: AxiosRequestConfig): AxiosPromise
delete(url: string, config?: AxiosRequestConfig): AxiosPromise
head(url: string, config?: AxiosRequestConfig): AxiosPromise
options(url: string, config?: AxiosRequestConfig): AxiosPromise
post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise
put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise
patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise
}
export interface AxiosInstance extends Axios {
(config: AxiosRequestConfig): AxiosPromise
}
export interface AxiosRequestConfig {
url?: string
// ...
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
First, we define an Axios type interface that describes the public methods of the Axios class. Then we define the AxiosInstance interface extending Axios, making it a hybrid type interface.
Additionally, the url property in the AxiosRequestConfig type interface has been changed to an optional property.
# Creating the Axios Class
We create an Axios class to implement the public methods defined in the interface. We create a core directory to house the core request flow code. Inside the core directory, we create the Axios.ts file.
core/Axios.ts
import { AxiosRequestConfig, AxiosPromise, Method } from '../types'
import dispatchRequest from './dispatchRequest'
export default class Axios {
request(config: AxiosRequestConfig): AxiosPromise {
return dispatchRequest(config)
}
get(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData('get', url, config)
}
delete(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData('delete', url, config)
}
head(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData('head', url, config)
}
options(url: string, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithoutData('options', url, config)
}
post(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithData('post', url, data, config)
}
put(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithData('put', url, data, config)
}
patch(url: string, data?: any, config?: AxiosRequestConfig): AxiosPromise {
return this._requestMethodWithData('patch', url, data, config)
}
_requestMethodWithoutData(method: Method, url: string, config?: AxiosRequestConfig) {
return this.request(
Object.assign(config || {}, {
method,
url
})
)
}
_requestMethodWithData(method: Method, url: string, data?: any, config?: AxiosRequestConfig) {
return this.request(
Object.assign(config || {}, {
method,
url,
data
})
)
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
The request method has the same functionality as our previous axios function. Since the axios function's purpose is to send requests, following modular programming principles, we extract this functionality into a separate module. Inside the core directory, we create the dispatchRequest method and move the relevant code from axios.ts there. We also move the xhr.ts file into the core directory.
core/dispatchRequest.ts:
import { AxiosPromise, AxiosRequestConfig, AxiosResponse } from '../types'
import xhr from './xhr'
import { buildURL } from '../helpers/url'
import { transformRequest, transformResponse } from '../helpers/data'
import { processHeaders } from '../helpers/headers'
export default function dispatchRequest(config: AxiosRequestConfig): AxiosPromise {
processConfig(config)
return xhr(config).then(res => {
return transformResponseData(res)
})
}
function processConfig(config: AxiosRequestConfig): void {
config.url = transformURL(config)
config.headers = transformHeaders(config)
config.data = transformRequestData(config)
}
function transformURL(config: AxiosRequestConfig): string {
const { url, params } = config
return buildURL(url, params)
}
function transformRequestData(config: AxiosRequestConfig): any {
return transformRequest(config.data)
}
function transformHeaders(config: AxiosRequestConfig) {
const { headers = {}, data } = config
return processHeaders(headers, data)
}
function transformResponseData(res: AxiosResponse): AxiosResponse {
res.data = transformResponse(res.data)
return res
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
Returning to the Axios.ts file, the get, delete, head, options, post, patch, and put methods are all syntactic sugar exposed to the user. Internally, they all send requests by calling the request method, with the only difference being a layer of config merging before the call.
# Hybrid Object Implementation
The implementation of the hybrid object is straightforward. First, this object is a function; second, it needs to include all prototype and instance properties of the Axios class. Let's start by implementing a helper function called extend.
helpers/util.ts
export function extend<T, U>(to: T, from: U): T & U {
for (const key in from) {
;(to as T & U)[key] = from[key] as any
}
return to as T & U
}
2
3
4
5
6
The extend method uses intersection types and type assertions. The ultimate goal of extend is to copy all properties from from to to, including prototype properties.
Next, we modify the axios.ts file. We use the factory pattern to create the axios hybrid object.
axios.ts:
import { AxiosInstance } from './types'
import Axios from './core/Axios'
import { extend } from './helpers/util'
function createInstance(): AxiosInstance {
const context = new Axios()
const instance = Axios.prototype.request.bind(context)
extend(instance, context)
return instance as AxiosInstance
}
const axios = createInstance()
export default axios
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Inside the createInstance factory function, we first instantiate an Axios instance called context, then create instance pointing to the Axios.prototype.request method with context bound as the context. We then use the extend method to copy all prototype and instance methods from context onto instance, achieving the hybrid object: instance is itself a function while also possessing all prototype and instance properties of the Axios class. Finally, we return this instance. Since TypeScript cannot correctly infer the type of instance here, we assert it as the AxiosInstance type.
This way, we create axios through the createInstance factory function. Calling axios directly is equivalent to executing the Axios class's request method to send a request. Of course, we can also call axios.get, axios.post, and other methods.
# Writing the Demo
Create an extend directory under the examples directory, and create index.html inside the extend directory:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<title>Extend example</title>
</head>
<body>
<script src="/__build__/extend.js"></script>
</body>
</html>
2
3
4
5
6
7
8
9
10
Then create app.ts as the entry file:
import axios from '../../src/index'
axios({
url: '/extend/post',
method: 'post',
data: {
msg: 'hi'
}
})
axios.request({
url: '/extend/post',
method: 'post',
data: {
msg: 'hello'
}
})
axios.get('/extend/get')
axios.options('/extend/options')
axios.delete('/extend/delete')
axios.head('/extend/head')
axios.post('/extend/post', { msg: 'post' })
axios.put('/extend/put', { msg: 'put' })
axios.patch('/extend/patch', { msg: 'patch' })
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
Then run npm run dev in the command line, open Chrome browser, and visit http://localhost:8080/ to access our demo. Navigate to the Extend directory, and through the developer tools' network panel, we can see the status of each request sent.
At this point, we have supported extending the axios API and turned it into a hybrid object. The official axios instance supports not only axios(config) but also passing 2 arguments axios(url, config), which involves the concept of function overloading. In the next section, we will implement this feature.