// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license. // This module is browser compatible. // deno-lint-ignore-file ban-types import { filterInPlace } from "./_utils.ts"; const { hasOwn } = Object; /** * Merges the two given Records, recursively merging any nested Records with * the second collection overriding the first in case of conflict * * For arrays, maps and sets, a merging strategy can be specified to either * "replace" values, or "merge" them instead. * Use "includeNonEnumerable" option to include non enumerable properties too. * * Example: * * ```ts * import { deepMerge } from "https://deno.land/std@$STD_VERSION/collections/mod.ts"; * import { assertEquals } from "https://deno.land/std@$STD_VERSION/testing/asserts.ts"; * * const a = {foo: true} * const b = {foo: {bar: true}} * * assertEquals(deepMerge(a, b), {foo: {bar: true}}); * ``` */ export function deepMerge< T extends Record, >( record: Partial>, other: Partial>, options?: Readonly, ): T; export function deepMerge< T extends Record, U extends Record, Options extends DeepMergeOptions, >( record: Readonly, other: Readonly, options?: Readonly, ): DeepMerge; export function deepMerge< T extends Record, U extends Record, Options extends DeepMergeOptions = { arrays: "merge"; sets: "merge"; maps: "merge"; }, >( record: Readonly, other: Readonly, options?: Readonly, ): DeepMerge { // Extract options // Clone left operand to avoid performing mutations in-place type Result = DeepMerge; const result: Partial = {}; const keys = new Set([ ...getKeys(record), ...getKeys(other), ]) as Set; // Iterate through each key of other object and use correct merging strategy for (const key of keys) { // Skip to prevent Object.prototype.__proto__ accessor property calls on non-Deno platforms if (key === "__proto__") { continue; } type ResultMember = Result[typeof key]; const a = record[key] as ResultMember; if (!hasOwn(other, key)) { result[key] = a; continue; } const b = other[key] as ResultMember; if (isNonNullObject(a) && isNonNullObject(b)) { result[key] = mergeObjects(a, b, options) as ResultMember; continue; } // Override value result[key] = b; } return result as Result; } function mergeObjects( left: Readonly>, right: Readonly>, options: Readonly = { arrays: "merge", sets: "merge", maps: "merge", }, ): Readonly> { // Recursively merge mergeable objects if (isMergeable(left) && isMergeable(right)) { return deepMerge(left, right); } if (isIterable(left) && isIterable(right)) { // Handle arrays if ((Array.isArray(left)) && (Array.isArray(right))) { if (options.arrays === "merge") { return left.concat(right); } return right; } // Handle maps if ((left instanceof Map) && (right instanceof Map)) { if (options.maps === "merge") { return new Map([ ...left, ...right, ]); } return right; } // Handle sets if ((left instanceof Set) && (right instanceof Set)) { if (options.sets === "merge") { return new Set([ ...left, ...right, ]); } return right; } } return right; } /** * Test whether a value is mergeable or not * Builtins that look like objects, null and user defined classes * are not considered mergeable (it means that reference will be copied) */ function isMergeable( value: NonNullable, ): value is Record { return Object.getPrototypeOf(value) === Object.prototype; } function isIterable( value: NonNullable, ): value is Iterable { return typeof (value as Iterable)[Symbol.iterator] === "function"; } function isNonNullObject(value: unknown): value is NonNullable { return value !== null && typeof value === "object"; } function getKeys(record: T): Array { const ret = Object.getOwnPropertySymbols(record) as Array; filterInPlace( ret, (key) => Object.prototype.propertyIsEnumerable.call(record, key), ); ret.push(...(Object.keys(record) as Array)); return ret; } /** Merging strategy */ export type MergingStrategy = "replace" | "merge"; /** Deep merge options */ export type DeepMergeOptions = { /** Merging strategy for arrays */ arrays?: MergingStrategy; /** Merging strategy for Maps */ maps?: MergingStrategy; /** Merging strategy for Sets */ sets?: MergingStrategy; }; /** * How does recursive typing works ? * * Deep merging process is handled through `DeepMerge` type. * If both T and U are Records, we recursively merge them, * else we treat them as primitives. * * Merging process is handled through `Merge` type, in which * we remove all maps, sets, arrays and records so we can handle them * separately depending on merging strategy: * * Merge< * {foo: string}, * {bar: string, baz: Set}, * > // "foo" and "bar" will be handled with `MergeRightOmitComplexes` * // "baz" will be handled with `MergeAll*` type * * `MergeRightOmitComplexes` will do the above: all T's * exclusive keys will be kept, though common ones with U will have their * typing overridden instead: * * MergeRightOmitComplexes< * {foo: string, baz: number}, * {foo: boolean, bar: string} * > // {baz: number, foo: boolean, bar: string} * // "baz" was kept from T * // "foo" was overridden by U's typing * // "bar" was added from U * * For Maps, Arrays, Sets and Records, we use `MergeAll*` utility * types. They will extract relevant data structure from both T and U * (providing that both have same data data structure, except for typing). * * From these, `*ValueType` will extract values (and keys) types to be * able to create a new data structure with an union typing from both * data structure of T and U: * * MergeAllSets< * {foo: Set}, * {foo: Set} * > // `SetValueType` will extract "number" for T * // `SetValueType` will extract "string" for U * // `MergeAllSets` will infer type as Set * // Process is similar for Maps, Arrays, and Sets * * `DeepMerge` is taking a third argument to be handle to * infer final typing depending on merging strategy: * * & (Options extends { sets: "replace" } ? PartialByType> * : MergeAllSets) * * In the above line, if "Options" have its merging strategy for Sets set to * "replace", instead of performing merging of Sets type, it will take the * typing from right operand (U) instead, effectively replacing the typing. * * An additional note, we use `ExpandRecursively` utility type to expand * the resulting typing and hide all the typing logic of deep merging so it is * more user friendly. */ /** Force intellisense to expand the typing to hide merging typings */ type ExpandRecursively = T extends Record ? T extends infer O ? { [K in keyof O]: ExpandRecursively } : never : T; /** Filter of keys matching a given type */ type PartialByType = { [K in keyof T as T[K] extends U ? K : never]: T[K]; }; /** Get set values type */ type SetValueType = T extends Set ? V : never; /** Merge all sets types definitions from keys present in both objects */ type MergeAllSets< T, U, X = PartialByType>, Y = PartialByType>, Z = { [K in keyof X & keyof Y]: Set | SetValueType>; }, > = Z; /** Get array values type */ type ArrayValueType = T extends Array ? V : never; /** Merge all sets types definitions from keys present in both objects */ type MergeAllArrays< T, U, X = PartialByType>, Y = PartialByType>, Z = { [K in keyof X & keyof Y]: Array< ArrayValueType | ArrayValueType >; }, > = Z; /** Get map values types */ type MapKeyType = T extends Map ? K : never; /** Get map values types */ type MapValueType = T extends Map ? V : never; /** Merge all sets types definitions from keys present in both objects */ type MergeAllMaps< T, U, X = PartialByType>, Y = PartialByType>, Z = { [K in keyof X & keyof Y]: Map< MapKeyType | MapKeyType, MapValueType | MapValueType >; }, > = Z; /** Merge all records types definitions from keys present in both objects */ type MergeAllRecords< T, U, Options, X = PartialByType>, Y = PartialByType>, Z = { [K in keyof X & keyof Y]: DeepMerge; }, > = Z; /** Exclude map, sets and array from type */ type OmitComplexes = Omit< T, keyof PartialByType< T, | Map | Set | Array | Record > >; /** Object with keys in either T or U but not in both */ type ObjectXorKeys< T, U, X = Omit & Omit, Y = { [K in keyof X]: X[K] }, > = Y; /** Merge two objects, with left precedence */ type MergeRightOmitComplexes< T, U, X = ObjectXorKeys & OmitComplexes<{ [K in keyof U]: U[K] }>, > = X; /** Merge two objects */ type Merge< T, U, Options, X = & MergeRightOmitComplexes & MergeAllRecords & (Options extends { sets: "replace" } ? PartialByType> : MergeAllSets) & (Options extends { arrays: "replace" } ? PartialByType> : MergeAllArrays) & (Options extends { maps: "replace" } ? PartialByType> : MergeAllMaps), > = ExpandRecursively; /** Merge deeply two objects */ export type DeepMerge< T, U, Options = Record, > = // Handle objects [T, U] extends [Record, Record] ? Merge : // Handle primitives T | U;