-
-
Notifications
You must be signed in to change notification settings - Fork 9
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
1 parent
d96f696
commit 8e3ba66
Showing
9 changed files
with
1,087 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,156 @@ | ||
import type { | ||
DeepMerge, | ||
DeepMergeArrays, | ||
DeepMergeMaps, | ||
DeepMergeRecords, | ||
DeepMergeSets, | ||
DeepMergeUnknowns, | ||
Property, | ||
} from "./types"; | ||
import { getKeys, getObjectType, ObjectType, objectHasProperty } from "./utils"; | ||
|
||
/** | ||
* Deeply merge two or more objects. | ||
* | ||
* @param objects - The objects to merge. | ||
*/ | ||
export function deepmerge<Ts extends readonly [unknown, ...unknown[]]>( | ||
...objects: readonly [...Ts] | ||
): DeepMerge<Ts>; | ||
|
||
/** | ||
* Deeply merge two or more objects. | ||
* | ||
* @param objects - The objects to merge. | ||
*/ | ||
export function deepmerge( | ||
...objects: Readonly<ReadonlyArray<unknown>> | ||
): unknown; | ||
export function deepmerge( | ||
...objects: Readonly<ReadonlyArray<unknown>> | ||
): unknown { | ||
if (objects.length === 0) { | ||
return {}; | ||
} | ||
if (objects.length === 1) { | ||
return objects[0]; | ||
} | ||
|
||
return objects.reduce(deepmergeUnknowns); | ||
} | ||
|
||
/** | ||
* Deeply merge two objects. | ||
* | ||
* @param x - The first object. | ||
* @param y - The second object. | ||
*/ | ||
function deepmergeUnknowns<T1, T2>(x: T1, y: T2): DeepMergeUnknowns<T1, T2> { | ||
const typeOfX = getObjectType(x); | ||
const typeOfY = getObjectType(y); | ||
|
||
if ( | ||
typeOfX !== typeOfY || | ||
typeOfX === ObjectType.NOT || | ||
typeOfX === ObjectType.OTHER | ||
) { | ||
return y as DeepMergeUnknowns<T1, T2>; | ||
} | ||
|
||
if (typeOfX === ObjectType.RECORD) { | ||
return mergeRecords( | ||
x as Readonly<Record<Property, unknown>>, | ||
y as Readonly<Record<Property, unknown>> | ||
) as DeepMergeUnknowns<T1, T2>; | ||
} | ||
|
||
if (typeOfX === ObjectType.ARRAY) { | ||
return mergeArrays( | ||
x as unknown as Readonly<ReadonlyArray<unknown>>, | ||
y as unknown as Readonly<ReadonlyArray<unknown>> | ||
) as DeepMergeUnknowns<T1, T2>; | ||
} | ||
|
||
if (typeOfX === ObjectType.SET) { | ||
return mergeSets( | ||
x as unknown as Readonly<ReadonlySet<unknown>>, | ||
y as unknown as Readonly<ReadonlySet<unknown>> | ||
) as unknown as DeepMergeUnknowns<T1, T2>; | ||
} | ||
|
||
return mergeMaps( | ||
x as unknown as Readonly<ReadonlyMap<unknown, unknown>>, | ||
y as unknown as Readonly<ReadonlyMap<unknown, unknown>> | ||
) as unknown as DeepMergeUnknowns<T1, T2>; | ||
} | ||
|
||
/** | ||
* Merge two records. | ||
* | ||
* @param x - The first records. | ||
* @param y - The second records. | ||
*/ | ||
function mergeRecords< | ||
T1 extends Readonly<Record<Property, unknown>>, | ||
T2 extends Readonly<Record<Property, unknown>> | ||
>(x: T1, y: T2) { | ||
return Object.fromEntries( | ||
[...getKeys([x, y])].map((key) => { | ||
const xHasKey = objectHasProperty(x, key); | ||
const yHasKey = objectHasProperty(y, key); | ||
|
||
if (xHasKey && yHasKey) { | ||
return [key, deepmergeUnknowns(x[key], y[key])]; | ||
} | ||
if (yHasKey) { | ||
return [key, y[key]]; | ||
} | ||
return [key, x[key]]; | ||
}) | ||
) as DeepMergeRecords<T1, T2>; | ||
} | ||
|
||
/** | ||
* Merge two arrays. | ||
* | ||
* @param x - The first array. | ||
* @param y - The second array. | ||
*/ | ||
function mergeArrays< | ||
T1 extends Readonly<ReadonlyArray<unknown>>, | ||
T2 extends Readonly<ReadonlyArray<unknown>> | ||
>(x: T1, y: T2) { | ||
return [...x, ...y] as DeepMergeArrays<T1, T2>; | ||
} | ||
|
||
/** | ||
* Merge two sets. | ||
* | ||
* @param x - The first sets. | ||
* @param y - The second sets. | ||
*/ | ||
function mergeSets< | ||
T1 extends Readonly<ReadonlySet<unknown>>, | ||
T2 extends Readonly<ReadonlySet<unknown>> | ||
>(x: T1, y: T2) { | ||
return new Set([...x, ...y]) as DeepMergeSets<T1, T2>; | ||
} | ||
|
||
/** | ||
* Merge two maps. | ||
* | ||
* @param x - The first maps. | ||
* @param y - The second maps. | ||
*/ | ||
function mergeMaps< | ||
T1 extends Readonly<ReadonlyMap<unknown, unknown>>, | ||
T2 extends Readonly<ReadonlyMap<unknown, unknown>> | ||
>(x: T1, y: T2) { | ||
return [x, y].reduce((mutableCarry, current) => { | ||
// eslint-disable-next-line functional/no-loop-statement -- using a loop here is more efficient. | ||
for (const [key, value] of current.entries()) { | ||
mutableCarry.set(key, deepmergeUnknowns(mutableCarry.get(key), value)); | ||
} | ||
return mutableCarry; | ||
}, new Map()) as DeepMergeMaps<T1, T2>; | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1 @@ | ||
export * from "./deepmerge"; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,175 @@ | ||
/** | ||
* Deep merge 1 or more types given in an array. | ||
*/ | ||
export type DeepMerge<Ts extends readonly [unknown, ...unknown[]]> = | ||
Ts extends readonly [infer T1, ...unknown[]] | ||
? Ts extends readonly [T1, infer T2, ...infer TRest] | ||
? TRest extends Readonly<ReadonlyArray<never>> | ||
? DeepMergeUnknowns<T1, T2> | ||
: DeepMergeUnknowns<T1, DeepMerge<[T2, ...TRest]>> | ||
: T1 | ||
: never; | ||
|
||
/** | ||
* Deep merge 2 types. | ||
*/ | ||
export type DeepMergeUnknowns<T1, T2> = And< | ||
IsArray<T1>, | ||
IsArray<T2> | ||
> extends true | ||
? DeepMergeArrays<T1, T2> | ||
: And<IsMap<T1>, IsMap<T2>> extends true | ||
? DeepMergeMaps<T1, T2> | ||
: And<IsSet<T1>, IsSet<T2>> extends true | ||
? DeepMergeSets<T1, T2> | ||
: And<IsRecord<T1>, IsRecord<T2>> extends true | ||
? DeepMergeRecords<T1, T2> | ||
: Leaf<T1, T2>; | ||
|
||
/** | ||
* A union of all the props that should not be included in type information for | ||
* merged records. | ||
*/ | ||
type BlacklistedRecordProps = "__proto__"; | ||
|
||
/** | ||
* Deep merge 2 non-array objects. | ||
*/ | ||
export type DeepMergeRecords<T1, T2> = FlatternAlias< | ||
Omit< | ||
// prettier-ignore | ||
{ | ||
-readonly [K in keyof T1]: DeepMergeRecordProps< | ||
ValueOfKey<T1, K>, | ||
ValueOfKey<T2, K> | ||
>; | ||
} & | ||
{ | ||
-readonly [K in keyof T2]: DeepMergeRecordProps< | ||
ValueOfKey<T1, K>, | ||
ValueOfKey<T2, K> | ||
>; | ||
}, | ||
BlacklistedRecordProps | ||
> | ||
>; | ||
|
||
/** | ||
* Deep merge 2 types that are known to be properties of an object being deeply | ||
* merged. | ||
*/ | ||
type DeepMergeRecordProps<T1, T2> = Or<IsNever<T1>, IsNever<T2>> extends true | ||
? Leaf<T1, T2> | ||
: DeepMergeUnknowns<T1, T2>; | ||
|
||
/** | ||
* Deep merge 2 arrays. | ||
*/ | ||
export type DeepMergeArrays<T1, T2> = T1 extends readonly [...infer E1] | ||
? T2 extends readonly [...infer E2] | ||
? [...E1, ...E2] | ||
: never | ||
: never; | ||
|
||
/** | ||
* Deep merge 2 sets. | ||
*/ | ||
export type DeepMergeSets<T1, T2> = T1 extends Set<infer E1> | ||
? T2 extends Set<infer E2> | ||
? Set<E1 | E2> | ||
: never | ||
: never; | ||
|
||
/** | ||
* Deep merge 2 maps. | ||
*/ | ||
export type DeepMergeMaps<T1, T2> = T1 extends Map<infer K1, infer V1> | ||
? T2 extends Map<infer K2, infer V2> | ||
? Map<K1 | K2, V1 | V2> | ||
: never | ||
: never; | ||
|
||
/** | ||
* Get the leaf type from 2 types that can't be merged. | ||
*/ | ||
type Leaf<T1, T2> = IsNever<T2> extends true ? T1 : T2; | ||
|
||
/** | ||
* Flatten a complex type such as a union or intersection of objects into a | ||
* single object. | ||
*/ | ||
type FlatternAlias<T> = { [P in keyof T]: T[P] } & {}; | ||
|
||
/** | ||
* Get the value of the given key in the given object. | ||
*/ | ||
type ValueOfKey<T, K> = K extends keyof T ? T[K] : never; | ||
|
||
/** | ||
* Safely test whether or not the first given types extends the second. | ||
* | ||
* Needed in particular for testing if a type is "never". | ||
*/ | ||
type Is<T1, T2> = [T1] extends [T2] ? true : false; | ||
|
||
/** | ||
* Safely test whether or not the given type is "never". | ||
*/ | ||
type IsNever<T> = Is<T, never>; | ||
|
||
/** | ||
* Returns whether or not the given type a record. | ||
*/ | ||
type IsRecord<T> = And< | ||
Not<IsNever<T>>, | ||
T extends Readonly<Record<Property, unknown>> ? true : false | ||
>; | ||
|
||
/** | ||
* Returns whether or not the given type is an array. | ||
*/ | ||
type IsArray<T> = And< | ||
Not<IsNever<T>>, | ||
T extends Readonly<ReadonlyArray<unknown>> ? true : false | ||
>; | ||
|
||
/** | ||
* Returns whether or not the given type is an set. | ||
* | ||
* Note: This may also return true for Maps. | ||
*/ | ||
type IsSet<T> = And< | ||
Not<IsNever<T>>, | ||
T extends Readonly<ReadonlySet<unknown>> ? true : false | ||
>; | ||
|
||
/** | ||
* Returns whether or not the given type is an map. | ||
*/ | ||
type IsMap<T> = And< | ||
Not<IsNever<T>>, | ||
T extends Readonly<ReadonlyMap<unknown, unknown>> ? true : false | ||
>; | ||
|
||
/** | ||
* And operator for types. | ||
*/ | ||
type And<T1 extends boolean, T2 extends boolean> = T1 extends false | ||
? false | ||
: T2; | ||
|
||
/** | ||
* Or operator for types. | ||
*/ | ||
type Or<T1 extends boolean, T2 extends boolean> = T1 extends true ? true : T2; | ||
|
||
/** | ||
* Not operator for types. | ||
*/ | ||
type Not<T extends boolean> = T extends true ? false : true; | ||
|
||
/** | ||
* A property that can index an object. | ||
*/ | ||
// eslint-disable-next-line @typescript-eslint/no-explicit-any | ||
export type Property = keyof any; |
Oops, something went wrong.