Skip to content

Commit

Permalink
feat: add basic functionality
Browse files Browse the repository at this point in the history
  • Loading branch information
RebeccaStevens committed Aug 24, 2021
1 parent d96f696 commit 8e3ba66
Show file tree
Hide file tree
Showing 9 changed files with 1,087 additions and 0 deletions.
156 changes: 156 additions & 0 deletions src/deepmerge.ts
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>;
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export * from "./deepmerge";
175 changes: 175 additions & 0 deletions src/types.ts
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;

0 comments on commit 8e3ba66

Please sign in to comment.