VDone Demo VDone Demo
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)

Nikolay Tuzov

Backend Developer
Home
  • Articles

    • JavaScript
  • Study Notes

    • JavaScript Tutorial
    • Professional JavaScript
    • ES6 Tutorial
    • Vue
    • React
    • TypeScript: Build Axios from Scratch
    • Git
    • TypeScript
    • JS Design Patterns
  • HTML
  • CSS
  • Technical Docs
  • GitHub Tips
  • Node.js
  • Blog Setup
  • Learning
  • Interviews
  • Miscellaneous
  • Practical Tips
  • Friends
About
Bookmarks
  • Categories
  • Tags
  • Archives
GitHub (opens new window)
  • 初识 TypeScript

  • TypeScript 常用语法

  • ts-axios 项目初始化

  • ts-axios 基础功能实现

  • ts-axios 异常情况处理

  • ts-axios 接口扩展

  • ts-axios 拦截器实现

  • ts-axios 配置化实现

    • Config Merging Design and Implementation
      • Requirements Analysis
      • Default Configuration
        • Defining Default Configuration
        • Adding to the axios Object
      • Config Merging and Strategies
        • Merge Method
        • Default Merge Strategy
        • Custom-Config-Only Merge Strategy
        • Complex Object Merge Strategy
      • Flatten Headers
      • Writing the Demo
    • Request and Response Configuration
    • Extending the create Static Interface
  • ts-axios 取消功能实现

  • ts-axios 更多功能实现

  • ts-axios 单元测试

  • ts-axios 部署与发布

  • 《TypeScript 从零实现 axios》
  • ts-axios 配置化实现
HuangYi
2020-01-05
Contents

Config Merging Design and Implementation

# Config Merging Design and Implementation

# Requirements Analysis

In previous chapters, we learned that we can pass a configuration when sending requests to determine different request behaviors. We also want ts-axios to have default configuration that defines some default behaviors. This way, when sending each request, the user-provided configuration can be merged with the defaults.

To stay consistent with the official axios library, we add a defaults property to the axios object representing the default configuration. You can even modify these defaults directly:

axios.defaults.headers.common['test'] = 123
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded'
axios.defaults.timeout = 2000
1
2
3

The default configuration for headers supports common and various request method fields. common means the property should be added for any type of request, while method means the property should only be added for that specific request method.

In the example above, we will add a test property to all request headers by default, and add a Content-Type property to post request headers by default.

# Default Configuration

# Defining Default Configuration

Next, let's implement the default configuration.

defaults.ts:

import { AxiosRequestConfig } from './types'

const defaults: AxiosRequestConfig = {
  method: 'get',

  timeout: 0,

  headers: {
    common: {
      Accept: 'application/json, text/plain, */*'
    }
  }
}

const methodsNoData = ['delete', 'get', 'head', 'options']

methodsNoData.forEach(method => {
  defaults.headers[method] = {}
})

const methodsWithData = ['post', 'put', 'patch']

methodsWithData.forEach(method => {
  defaults.headers[method] = {
    'Content-Type': 'application/x-www-form-urlencoded'
  }
})

export default defaults
1
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

We defined the defaults constant, which includes the default request method, timeout, and headers configuration.

In the future, we will add more default configurations based on new requirements.

# Adding to the axios Object

Based on the requirements, we need to add a defaults property to the axios object representing the default configuration:

export default class Axios {
  defaults: AxiosRequestConfig
  interceptors: Interceptors

  constructor(initConfig: AxiosRequestConfig) {
    this.defaults = initConfig
    this.interceptors = {
      request: new InterceptorManager<AxiosRequestConfig>(),
      response: new InterceptorManager<AxiosResponse>()
    }
  }
  // ...
}
1
2
3
4
5
6
7
8
9
10
11
12
13

We added a defaults member property to the Axios class, and the Axios constructor now accepts an initConfig object, assigning initConfig to this.defaults.

Then we modify the createInstance method to support passing a config object.

import defaults from './defaults'

function createInstance(config: AxiosRequestConfig): AxiosStatic {
  const context = new Axios(config)
  const instance = Axios.prototype.request.bind(context)

  // extend(instance, Axios.prototype, context)

  extend(instance, context)

  return instance as AxiosStatic
}

const axios = createInstance(defaults)
1
2
3
4
5
6
7
8
9
10
11
12
13
14

This way, when we create the axios object by executing createInstance, we can pass in the default configuration.

# Config Merging and Strategies

After defining the default configuration, we need to merge the custom configuration with the defaults when sending each request. This is not a simple merge of two plain objects -- different fields have different merging strategies. For example:

config1 = {
  method: 'get',

  timeout: 0,

  headers: {
    common: {
      Accept: 'application/json, text/plain, */*'
    }
  }
}

config2 = {
  url: '/config/post',
  method: 'post',
  data: {
    a: 1
  },
  headers: {
    test: '321'
  }
}

merged = {
  url: '/config/post',
  method: 'post',
  data: {
    a: 1
  },
  timeout: 0,
  headers: {
    common: {
      Accept: 'application/json, text/plain, */*'
    }
    test: '321'
  }
}
1
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

We implement the merge method in core/mergeConfig.ts.

# Merge Method

export default function mergeConfig(
  config1: AxiosRequestConfig,
  config2?: AxiosRequestConfig
): AxiosRequestConfig {
  if (!config2) {
    config2 = {}
  }

  const config = Object.create(null)

  for (let key in config2) {
    mergeField(key)
  }

  for (let key in config1) {
    if (!config2[key]) {
      mergeField(key)
    }
  }

  function mergeField(key: string): void {
    const strat = strats[key] || defaultStrat
    config[key] = strat(config1[key], config2![key])
  }

  return config
}
1
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

The overall approach of the merge method is to iterate over the properties of config1 and config2, executing the mergeField method for merging. Here config1 represents the default configuration, and config2 represents the custom configuration.

During iteration, we access properties using index notation like config2[key], so we need to add a string index signature to the AxiosRequestConfig interface definition:

export interface AxiosRequestConfig {
  // ...

  [propName: string]: any
}
1
2
3
4
5

In the mergeField method, we use different merge strategies for different properties.

# Default Merge Strategy

This is the merge strategy for most properties:

function defaultStrat(val1: any, val2: any): any {
  return typeof val2 !== 'undefined' ? val2 : val1
}
1
2
3

It's straightforward -- if val2 exists, return val2; otherwise return val1. In other words, if a property is defined in the custom configuration, use the custom value; otherwise use the default.

# Custom-Config-Only Merge Strategy

For properties like url, params, and data, the merge strategy is as follows:

function fromVal2Strat(val1: any, val2: any): any {
  if (typeof val2 !== 'undefined') {
    return val2
  }
}

const stratKeysFromVal2 = ['url', 'params', 'data']

stratKeysFromVal2.forEach(key => {
  strats[key] = fromVal2Strat
})
1
2
3
4
5
6
7
8
9
10
11

For properties like url, params, and data, default values are meaningless since they are strongly tied to each individual request, so we only get them from the custom configuration.

# Complex Object Merge Strategy

For properties like headers, the merge strategy is as follows:

function deepMergeStrat(val1: any, val2: any): any {
  if (isPlainObject(val2)) {
    return deepMerge(val1, val2)
  } else if (typeof val2 !== 'undefined') {
    return val2
  } else if (isPlainObject(val1)) {
    return deepMerge(val1)
  } else if (typeof val1 !== 'undefined') {
    return val1
  }
}

const stratKeysDeepMerge = ['headers']

stratKeysDeepMerge.forEach(key => {
  strats[key] = deepMergeStrat
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

helpers/util.ts:

export function deepMerge(...objs: any[]): any {
  const result = Object.create(null)

  objs.forEach(obj => {
    if (obj) {
      Object.keys(obj).forEach(key => {
        const val = obj[key]
        if (isPlainObject(val)) {
          if (isPlainObject(result[key])) {
            result[key] = deepMerge(result[key], val)
          } else {
            result[key] = deepMerge({}, val)
          }
        } else {
          result[key] = val
        }
      })
    }
  })

  return result
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

For complex object properties like headers, we need to use deep copy. We also handle other cases since they might be plain non-object values. When we cover authentication later, the auth property will also use this merge strategy.

Finally, we add the config merging logic to the request method:

config = mergeConfig(this.defaults, config)
1

# Flatten Headers

After merging, the headers in the configuration is a complex object with additional properties like common, post, get, etc. The values within these properties are what we actually want to add to the request header.

For example:

headers: {
  common: {
    Accept: 'application/json, text/plain, */*'
  },
  post: {
    'Content-Type':'application/x-www-form-urlencoded'
  }
}
1
2
3
4
5
6
7
8

We need to flatten it to a single level, like:

headers: {
  Accept: 'application/json, text/plain, */*',
 'Content-Type':'application/x-www-form-urlencoded'
}
1
2
3
4

Note that for header fields defined in common, we need to extract all of them. For method-specific ones like post or get, we need to match them with the request method.

Next, let's implement the flattenHeaders method.

helpers/header.ts:

export function flattenHeaders(headers: any, method: Method): any {
  if (!headers) {
    return headers
  }
  headers = deepMerge(headers.common || {}, headers[method] || {}, headers)

  const methodsToDelete = ['delete', 'get', 'head', 'options', 'post', 'put', 'patch', 'common']

  methodsToDelete.forEach(method => {
    delete headers[method]
  })

  return headers
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14

We use deepMerge to copy the common and post properties to the top-level headers, then delete the common, post, and similar properties.

Then we execute this logic just before actually sending the request.

core/dispatchRequest.ts:

function processConfig(config: AxiosRequestConfig): void {
  config.url = transformURL(config)
  config.headers = transformHeaders(config)
  config.data = transformRequestData(config)
  config.headers = flattenHeaders(config.headers, config.method!)
}
1
2
3
4
5
6

This ensures that the headers in our configuration can be correctly added to the request header.

# Writing the Demo

Create a config directory under the examples directory, and create index.html inside the config directory:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <title>Config example</title>
  </head>
  <body>
    <script src="/__build__/config.js"></script>
  </body>
</html>
1
2
3
4
5
6
7
8
9
10

Then create app.ts as the entry file:

import axios from '../../src/index'
import qs from 'qs'

axios.defaults.headers.common['test2'] = 123

axios({
  url: '/config/post',
  method: 'post',
  data: qs.stringify({
    a: 1
  }),
  headers: {
    test: '321'
  }
}).then((res) => {
  console.log(res.data)
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

In this example, we additionally imported the qs library, which is a query string parsing and stringification library.

For instance, in our example, {a:1} becomes a=1 after going through qs.stringify.

Since our example added post and common headers to the defaults, we merge the configuration before the request. As a result, the Content-Type field is added to our request header with the value application/x-www-form-urlencoded; we also added the test2 field with the value 123.

At this point, the config merging logic is fully implemented. In the earlier chapters when building the basic functionality of axios, we handled both request data and response data. The official axios library includes this logic as part of the default configuration, meaning users can modify the logic for processing request and response data. In the next section, we will implement this feature.

Edit (opens new window)
#TypeScript
Last Updated: 2026/03/21, 12:14:36
Interceptor Design and Implementation
Request and Response Configuration

← Interceptor Design and Implementation Request and Response Configuration→

Recent Updates
01
How I Discovered Disposable Email — A True Story
06-12
02
Animations in Grid Layout
09-15
03
Renaming a Git Branch
08-11
More Articles >
Theme by VDone | Copyright © 2026-2026 Nikolay Tuzov | MIT License | Telegram
  • Auto
  • Light Mode
  • Dark Mode
  • Reading Mode