// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. // This module is browser compatible. import { filterInPlace } from "./_utils.ts"; /** * 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. * * @typeParam T Type of the first record * * @param record First record to merge. * @param other Second record to merge. * @param options Merging options. * * @returns A new record with the merged values. * * @example Merge objects * ```ts * import { deepMerge } from "@std/collections/deep-merge"; * import { assertEquals } from "@std/assert"; * * const a = { foo: true }; * const b = { foo: { bar: true } }; * * const result = deepMerge(a, b); * * const expected = { foo: { bar: true } }; * * assertEquals(result, expected); * ``` * * @example Merge arrays * ```ts * import { deepMerge } from "@std/collections/deep-merge"; * import { assertEquals } from "@std/assert"; * * const a = { foo: [1, 2] }; * const b = { foo: [3, 4] }; * * const result = deepMerge(a, b); * * const expected = { foo: [1, 2, 3, 4] }; * * assertEquals(result, expected); * ``` * * @example Merge maps * ```ts * import { deepMerge } from "@std/collections/deep-merge"; * import { assertEquals } from "@std/assert"; * * const a = { foo: new Map([["a", 1]]) }; * const b = { foo: new Map([["b", 2]]) }; * * const result = deepMerge(a, b); * * const expected = { foo: new Map([["a", 1], ["b", 2]]) }; * * assertEquals(result, expected); * ``` * * @example Merge sets * ```ts * import { deepMerge } from "@std/collections/deep-merge"; * import { assertEquals } from "@std/assert"; * * const a = { foo: new Set([1]) }; * const b = { foo: new Set([2]) }; * * const result = deepMerge(a, b); * * const expected = { foo: new Set([1, 2]) }; * * assertEquals(result, expected); * ``` * * @example Merge with custom options * ```ts * import { deepMerge } from "@std/collections/deep-merge"; * import { assertEquals } from "@std/assert"; * * const a = { foo: [1, 2] }; * const b = { foo: [3, 4] }; * * const result = deepMerge(a, b, { arrays: "replace" }); * * const expected = { foo: [3, 4] }; * * assertEquals(result, expected); * ``` */ export function deepMerge< T extends Record, >( record: Partial>, other: Partial>, options?: Readonly, ): T; /** * 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. * * @typeParam T Type of the first record * @typeParam U Type of the second record * @typeParam Options Merging options * * @param record First record to merge. * @param other Second record to merge. * @param options Merging options. * * @returns A new record with the merged values. * * @example Merge objects * ```ts * import { deepMerge } from "@std/collections/deep-merge"; * import { assertEquals } from "@std/assert"; * * const a = { foo: true }; * const b = { foo: { bar: true } }; * * const result = deepMerge(a, b); * * const expected = { foo: { bar: true } }; * * assertEquals(result, expected); * ``` * * @example Merge arrays * ```ts * import { deepMerge } from "@std/collections/deep-merge"; * import { assertEquals } from "@std/assert"; * * const a = { foo: [1, 2] }; * const b = { foo: [3, 4] }; * * const result = deepMerge(a, b); * * const expected = { foo: [1, 2, 3, 4] }; * * assertEquals(result, expected); * ``` * * @example Merge maps * ```ts * import { deepMerge } from "@std/collections/deep-merge"; * import { assertEquals } from "@std/assert"; * * const a = { foo: new Map([["a", 1]]) }; * const b = { foo: new Map([["b", 2]]) }; * * const result = deepMerge(a, b); * * const expected = { foo: new Map([["a", 1], ["b", 2]]) }; * * assertEquals(result, expected); * ``` * * @example Merge sets * ```ts * import { deepMerge } from "@std/collections/deep-merge"; * import { assertEquals } from "@std/assert"; * * const a = { foo: new Set([1]) }; * const b = { foo: new Set([2]) }; * * const result = deepMerge(a, b); * * const expected = { foo: new Set([1, 2]) }; * * assertEquals(result, expected); * ``` * * @example Merge with custom options * ```ts * import { deepMerge } from "@std/collections/deep-merge"; * import { assertEquals } from "@std/assert"; * * const a = { foo: [1, 2] }; * const b = { foo: [3, 4] }; * * const result = deepMerge(a, b, { arrays: "replace" }); * * const expected = { foo: [3, 4] }; * * assertEquals(result, expected); * ``` */ 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 { return deepMergeInternal(record, other, new Set(), options); } function deepMergeInternal< T extends Record, U extends Record, Options extends DeepMergeOptions = { arrays: "merge"; sets: "merge"; maps: "merge"; }, >( record: Readonly, other: Readonly, seen: Set>, options?: Readonly, ) { // 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 (!Object.hasOwn(other, key)) { result[key] = a; continue; } const b = other[key] as ResultMember; if ( isNonNullObject(a) && isNonNullObject(b) && !seen.has(a) && !seen.has(b) ) { seen.add(a); seen.add(b); result[key] = mergeObjects(a, b, seen, options) as ResultMember; continue; } // Override value result[key] = b; } return result as Result; } function mergeObjects( left: Readonly>>, right: Readonly>>, seen: Set>, options: Readonly = { arrays: "merge", sets: "merge", maps: "merge", }, ): Readonly | Iterable>> { // Recursively merge mergeable objects if (isMergeable(left) && isMergeable(right)) { return deepMergeInternal(left, right, seen, options); } 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 result = Object.getOwnPropertySymbols(record) as Array; filterInPlace( result, (key) => Object.prototype.propertyIsEnumerable.call(record, key), ); result.push(...(Object.keys(record) as Array)); return result; } /** Merging strategy */ export type MergingStrategy = "replace" | "merge"; /** Options for {@linkcode deepMerge}. */ export type DeepMergeOptions = { /** * Merging strategy for arrays * * @default {"merge"} */ arrays?: MergingStrategy; /** * Merging strategy for maps. * * @default {"merge"} */ maps?: MergingStrategy; /** * Merging strategy for sets. * * @default {"merge"} */ 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 */ export type ExpandRecursively = T extends Record ? T extends infer O ? { [K in keyof O]: ExpandRecursively } : never : T; /** Filter of keys matching a given type */ export type PartialByType = { [K in keyof T as T[K] extends U ? K : never]: T[K]; }; /** Get set values type */ export type SetValueType = T extends Set ? V : never; /** Merge all sets types definitions from keys present in both objects */ export type MergeAllSets< T, U, X = PartialByType>, Y = PartialByType>, Z = { [K in keyof X & keyof Y]: Set | SetValueType>; }, > = Z; /** Get array values type */ export type ArrayValueType = T extends Array ? V : never; /** Merge all sets types definitions from keys present in both objects */ export type MergeAllArrays< T, U, X = PartialByType>, Y = PartialByType>, Z = { [K in keyof X & keyof Y]: Array< ArrayValueType | ArrayValueType >; }, > = Z; /** Get map values types */ export type MapKeyType = T extends Map ? K : never; /** Get map values types */ export type MapValueType = T extends Map ? V : never; /** Merge all sets types definitions from keys present in both objects */ export 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 */ export 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 */ export type OmitComplexes = Omit< T, keyof PartialByType< T, | Map | Set | Array | Record > >; /** Object with keys in either T or U but not in both */ export type ObjectXorKeys< T, U, X = Omit & Omit, Y = { [K in keyof X]: X[K] }, > = Y; /** Merge two objects, with left precedence */ export type MergeRightOmitComplexes< T, U, X = ObjectXorKeys & OmitComplexes<{ [K in keyof U]: U[K] }>, > = X; /** Merge two objects */ export 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;