/
main.d.ts
165 lines (158 loc) · 5.23 KB
/
main.d.ts
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
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
import type { Updates } from 'set-array'
/**
* Merge `firstValue` and `secondValue`.
*
* Any object can change the merge mode using a `_merge` property with
* value `"deep"` (default), `"shallow"`, `"set"` or `"delete"`.
*
* Arrays can be merged using objects where the keys are the array indices.
* Items can be updated, merged, added, inserted, appended, prepended, deleted
* or set.
*
* `firstValue` and `secondValue` are not modified. Plain objects and arrays
* are deeply cloned. Inherited and non-enumerable properties are ignored.
*
* @example
* ```js
* // Deep merge
* declarativeMerge({ a: 1, b: { c: 2 }, d: 3 }, { a: 10, b: { e: 20 } })
* // { a: 10, b: { c: 2, e: 20 }, d: 3 }
*
* // Shallow merge
* declarativeMerge(
* { a: 1, b: { c: 2 }, d: 3 },
* { a: 10, b: { e: 20 }, _merge: 'shallow' },
* )
* // { a: 10, b: { e: 20 }, d: 3 }
*
* // No merge
* declarativeMerge(
* { a: 1, b: { c: 2 }, d: 3 },
* { a: 10, b: { e: 20 }, _merge: 'set' },
* )
* // { a: 10, b: { e: 20 } }
*
* // `_merge` can be specified in nested objects
* declarativeMerge(
* { a: 1, b: { c: 2 }, d: 3 },
* { a: 10, b: { e: 20, _merge: 'set' } },
* )
* // { a: 10, b: { e: 20 }, d: 3 }
*
* declarativeMerge(
* { a: 1, b: { c: 2 }, d: 3 },
* { a: 10, b: { e: 20, _merge: 'deep' }, _merge: 'set' },
* )
* // { a: 10, b: { c: 2, e: 20 } }
*
* // Delete
* declarativeMerge(
* { a: 1, b: { c: 2 }, d: 3 },
* { a: 10, b: { e: 20, _merge: 'delete' } },
* )
* // { a: 10, d: 3 }
*
* // By default, arrays override each other
* declarativeMerge({ one: ['a', 'b', 'c'] }, { one: ['X', 'Y'] }) // { one: ['X', 'Y'] }
*
* // They can be updated instead using an object where the keys are the array
* // indices (before any updates).
* declarativeMerge(
* { one: ['a', 'b', 'c'], two: 2 },
* { one: { 1: 'X' }, three: 3 },
* )
* // { one: ['a', 'X', 'c'], two: 2, three: 3 }
*
* // This works on top-level arrays too
* declarativeMerge(['a', 'b', 'c'], { 1: 'X', 2: 'Y' }) // ['a', 'X', 'Y']
*
* // If the new array items are objects, they are merged
* declarativeMerge(
* [{ id: 'a' }, { id: 'b', value: { name: 'Ann' } }, { id: 'c' }],
* { 1: { value: { color: 'red' } } },
* )
* // [{ id: 'a' }, { id: 'b', value: { name: 'Ann', color: 'red' } }, { id: 'c' }]
*
* declarativeMerge(
* [{ id: 'a' }, { id: 'b', value: { name: 'Ann' } }, { id: 'c' }],
* { 1: { value: { color: 'red' }, _merge: 'shallow' } },
* )
* // [{ id: 'a' }, { id: 'b', value: { color: 'red' } }, { id: 'c' }]
*
* // Indices
* declarativeMerge(['a', 'b', 'c'], { '*': 'X' }) // ['X', 'X', 'X']
* declarativeMerge(['a', 'b', 'c'], { '-1': 'X' }) // ['a', 'b', 'X']
* declarativeMerge(['a', 'b', 'c'], { 4: 'X' }) // ['a', 'b', 'c', undefined, 'X']
*
* // Array of items can be used
* declarativeMerge(['a', 'b', 'c'], { 1: ['X', 'Y'] }) // ['a', 'X', 'Y', 'c']
* declarativeMerge(['a', 'b', 'c'], { 1: ['X'] }) // ['a', 'X', 'c']
* declarativeMerge(['a', 'b', 'c'], { 1: [['X']] }) // ['a', ['X'], 'c']
*
* // If the key ends with +, items are prepended, not replaced
* declarativeMerge(['a', 'b', 'c'], { '1+': 'X' }) // ['a', 'X', 'b', 'c']
*
* // Append
* declarativeMerge(['a', 'b', 'c'], { '-0': 'X' }) // ['a', 'b', 'c', 'X']
* declarativeMerge(['a', 'b', 'c'], { '-0': ['X', 'Y'] }) // ['a', 'b', 'c', 'X', 'Y']
*
* // Prepend
* declarativeMerge(['a', 'b', 'c'], { '0+': ['X', 'Y'] }) // ['X', 'Y', 'a', 'b', 'c']
*
* // Delete
* declarativeMerge(['a', 'b', 'c'], { 1: [] }) // ['a', 'c']
*
* // Set
* declarativeMerge({}, { one: { 0: 'X', 2: 'Z' } }) // { one: ['X', undefined, 'Z'] }
* declarativeMerge({ one: true }, { one: { 0: 'X', 2: 'Z' } }) // { one: ['X', undefined, 'Z'] }
* ```
*/
export default function declarativeMerge<T, KeyOpt extends Key = DefaultKey>(
firstValue: T,
secondValue: SecondValue<T, NoInfer<KeyOpt>>,
options?: Options<KeyOpt>,
): T
// Ensure `T` is not inferred.
// See https://github.com/microsoft/TypeScript/issues/14829
type NoInfer<T> = T extends unknown ? T : T
export interface Options<KeyOpt extends Key = DefaultKey> {
/**
* Name of the property used to specify the merge mode.
* Symbols can be useful to prevent injections when the input is user-provided.
*
* @default "_merge"
*
* @example
* ```js
* declarativeMerge({ a: 1 }, { b: 2, _mergeMode: 'set' }, { key: '_mergeMode' }) // { b: 2 }
*
* const mergeMode = Symbol('mergeMode')
* declarativeMerge({ a: 1 }, { b: 2, [mergeMode]: 'set' }, { key: mergeMode }) // { b: 2 }
* ```
*/
key?: KeyOpt
}
/**
* `key` option's type
*/
type Key = string | symbol
/**
* `key` option's default value
*/
type DefaultKey = '_merge'
/**
* The second value has the same shape as the first except:
* - Objects can modify the merge mode using a `_merge` property
* - Arrays can be "updates" objects instead like { [index]: item, ... }
*/
type SecondValue<T, KeyOpt extends Key> = T extends (infer ArrayItem)[]
? SecondValue<ArrayItem, KeyOpt>[] | Updates<SecondValue<ArrayItem, KeyOpt>>
: T extends object
? {
[Prop in Exclude<keyof T, KeyOpt>]?: SecondValue<T[Prop], KeyOpt>
} & { [KeyProp in KeyOpt]?: MergeMode }
: T
/**
* Modifies the merge mode.
*/
type MergeMode = 'deep' | 'shallow' | 'set' | 'delete'