-
Notifications
You must be signed in to change notification settings - Fork 10
/
index.ts
135 lines (113 loc) · 3.39 KB
/
index.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
type TAllKeys<T> = T extends any ? keyof T : never;
type TIndexValue<T, K extends PropertyKey, D = never> = T extends any
? K extends keyof T
? T[K]
: D
: never;
type TPartialKeys<T, K extends keyof T> = Omit<T, K> &
Partial<Pick<T, K>> extends infer O
? { [P in keyof O]: O[P] }
: never;
type TFunction = (...a: any[]) => any;
type TPrimitives =
| string
| number
| boolean
| bigint
| symbol
| Date
| TFunction;
type TMerged<T> = [T] extends [Array<any>]
? { [K in keyof T]: TMerged<T[K]> }
: [T] extends [TPrimitives]
? T
: [T] extends [object]
? TPartialKeys<{ [K in TAllKeys<T>]: TMerged<TIndexValue<T, K>> }, never>
: T;
// istanbul ignore next
const isObject = (obj: any) => {
if (typeof obj === "object" && obj !== null) {
if (typeof Object.getPrototypeOf === "function") {
const prototype = Object.getPrototypeOf(obj);
return prototype === Object.prototype || prototype === null;
}
return Object.prototype.toString.call(obj) === "[object Object]";
}
return false;
};
interface IObject {
[key: string]: any;
}
export const merge = <T extends IObject[]>(...objects: T): TMerged<T[number]> =>
objects.reduce((result, current) => {
if (Array.isArray(current)) {
throw new TypeError(
"Arguments provided to ts-deepmerge must be objects, not arrays.",
);
}
Object.keys(current).forEach((key) => {
if (["__proto__", "constructor", "prototype"].includes(key)) {
return;
}
if (Array.isArray(result[key]) && Array.isArray(current[key])) {
result[key] = merge.options.mergeArrays
? merge.options.uniqueArrayItems
? Array.from(
new Set((result[key] as unknown[]).concat(current[key])),
)
: [...result[key], ...current[key]]
: current[key];
} else if (isObject(result[key]) && isObject(current[key])) {
result[key] = merge(result[key] as IObject, current[key] as IObject);
} else {
result[key] =
current[key] === undefined
? merge.options.allowUndefinedOverrides
? current[key]
: result[key]
: current[key];
}
});
return result;
}, {}) as any;
interface IOptions {
/**
* When `true`, values explicitly provided as `undefined` will override existing values, though properties that are simply omitted won't affect anything.
* When `false`, values explicitly provided as `undefined` won't override existing values.
*
* Default: `true`
*/
allowUndefinedOverrides: boolean;
/**
* When `true` it will merge array properties.
* When `false` it will replace array properties with the last instance entirely instead of merging their contents.
*
* Default: `true`
*/
mergeArrays: boolean;
/**
* When `true` it will ensure there are no duplicate array items.
* When `false` it will allow duplicates when merging arrays.
*
* Default: `true`
*/
uniqueArrayItems: boolean;
}
const defaultOptions: IOptions = {
allowUndefinedOverrides: true,
mergeArrays: true,
uniqueArrayItems: true,
};
merge.options = defaultOptions;
merge.withOptions = <T extends IObject[]>(
options: Partial<IOptions>,
...objects: T
) => {
merge.options = {
...defaultOptions,
...options,
};
const result = merge(...objects);
merge.options = defaultOptions;
return result;
};