Skip to main content
Deno 2 is finally here šŸŽ‰ļø
Learn more
Module

x/deepmergets/docs/deepmergeCustom.md

Deeply merge 2 or more objects respecting type information.
Go to Latest
File

Deepmerge Custom

deepmergeCustom allows you to customize the deepmerge function. It is a higher-order function; that is to say it returns a new customized deepmerge function.

Customizing the return type

If you want to customize the deepmerge function, you probably also want the return type of the result to be correct too.
Unfortunately however, due to TypeScript limitations, we can not automatically infer this. In order to get the correct return type, you need to provide us with type information about how you have customized the function (we do the very same to define the default configuration).

We need to use HKTs (higher-kinded types) in order to generate the right output type. But again, unfortunately, TypeScript does not support HKTs. Luckily however, there is a workaround. To use HKTs, we alias the type to a string type (a URI) and simply refer to that type by its alias until we need to resolve it.

Hereā€™s a simple example that creates a custom deepmerge function that does not merge arrays.

import type { DeepMergeLeafURI } from "deepmerge-ts";
import { deepmergeCustom } from "deepmerge-ts";

const customDeepmerge = deepmergeCustom<{
  DeepMergeArraysURI: DeepMergeLeafURI; // <-- Needed for correct output type.
}>({
  mergeArrays: false,
});

const x = { foo: [1, 2], bar: [3, 4] };
const y = { foo: [5, 6] };

customDeepmerge(x, y); // => { foo: [5, 6], bar: [3, 4] }

When resolving a HKT, we use a lookup inside an interface called DeepMergeMergeFunctionURItoKind. This interface needs to contain all the mappings of the URIs to their actual type.

When defining your own HKT for use with deepmerge, you need to extend this interface with your mapping. This can be done using Declaration Merging by declaring a module block for this library and defining the same interface.

declare module "deepmerge-ts" {
  interface DeepMergeMergeFunctionURItoKind<Ts extends ReadonlyArray<unknown>, MF extends DeepMergeMergeFunctionsURIs, M> {
    readonly MyCustomMergeURI: MyValue;
  }
}

Hereā€™s an example of creating a custom deepmerge function that amalgamates dates into an array.

import type { DeepMergeLeaf, DeepMergeMergeFunctionURItoKind, DeepMergeMergeFunctionsURIs } from "deepmerge-ts";
import { deepmergeCustom } from "deepmerge-ts";

const customizedDeepmerge = deepmergeCustom<{
  DeepMergeOthersURI: "MyDeepMergeDatesURI"; // <-- Needed for correct output type.
}>({
  mergeOthers: (values, utils, meta) => {
    // If every value is a date, the return the amalgamated array.
    if (values.every((value) => value instanceof Date)) {
      return values;
    }
    // Otherwise, use the default merging strategy.
    return utils.defaultMergeFunctions.mergeOthers(values);
  },
});

const x = { foo: new Date("2020-01-01") };
const y = { foo: new Date("2021-02-02") };
const z = { foo: new Date("2022-03-03") };

customDeepmerge(x, y, z); // => { foo: [Date, Date, Date] }

declare module "deepmerge-ts" {
  interface DeepMergeMergeFunctionURItoKind<
    Ts extends ReadonlyArray<unknown>,
    MF extends DeepMergeMergeFunctionsURIs,
    M
  > {
    readonly MyDeepMergeDatesURI: EveryIsDate<Ts> extends true ? Ts : DeepMergeLeaf<Ts>;
  }
}

type EveryIsDate<Ts extends ReadonlyArray<unknown>> = Ts extends readonly [infer Head, ...infer Rest]
  ? Head extends Date
    ? EveryIsDate<Rest>
    : false
  : true;

Note: If you want to use HKTs in your own project, not related to deepmerge-ts, we recommend checking out fp-ts.

Meta Data

We provide a simple object of meta data that states the key that the values being merged were under.

Hereā€™s an example that creates a custom deepmerge function that merges numbers differently based on the key they were under.

import type { DeepMergeLeaf, DeepMergeMergeFunctionURItoKind, DeepMergeMergeFunctionsURIs } from "deepmerge-ts";
import { deepmergeCustom } from "deepmerge-ts";

const customizedDeepmerge = deepmergeCustom({
  mergeOthers: (values, utils, meta) => {
    if (meta !== undefined && areAllNumbers(values)) {
      const { key } = meta;
      const numbers: ReadonlyArray<number> = values;

      if (key === "sum") {
        return numbers.reduce((sum, value) => sum + value);
      }
      if (key === "product") {
        return numbers.reduce((prod, value) => prod * value);
      }
      if (key === "mean") {
        return numbers.reduce((sum, value) => sum + value) / numbers.length;
      }
    }

    return utils.defaultMergeFunctions.mergeOthers(values);
  },
});

function areAllNumbers(values: ReadonlyArray<unknown>): values is ReadonlyArray<number> {
  return values.every((value) => typeof value === "number");
}

const v = { sum: 1, product: 2, mean: 3 };
const x = { sum: 4, product: 5, mean: 6 };
const y = { sum: 7, product: 8, mean: 9 };
const z = { sum: 10, product: 11, mean: 12 };

customizedDeepmerge(v, x, y, z); // => { sum: 22, product: 880, mean: 7.5 }

Customizing the Meta Data

You can customize the meta data that is passed to the merge functions by providing a metaDataUpdater function.

Hereā€™s an example that uses custom metadata that accumulates the full key path.

import type { DeepMergeLeaf, DeepMergeMergeFunctionURItoKind, DeepMergeMergeFunctionsURIs } from "deepmerge-ts";
import { deepmergeCustom } from "deepmerge-ts";

const customizedDeepmerge = deepmergeCustom<
  // Change the return type of `mergeOthers`.
  {
    DeepMergeOthersURI: "KeyPathBasedMerge";
  },
  // Change the meta data type.
  {
    keyPath: ReadonlyArray<PropertyKey>;
  }
>({
  // Customize what the actual meta data.
  metaDataUpdater: (previousMeta, metaMeta) => {
    if (previousMeta === undefined) {
      return { keyPath: [] };
    }
    return {
      ...metaMeta,
      keyPath: [...previousMeta.keyPath, metaMeta.key],
    };
  },
  // Use the meta data when merging others.
  mergeOthers: (values, utils, meta) => {
    if (
      meta !== undefined &&
      meta.keyPath.length >= 2 &&
      meta.keyPath[meta.keyPath.length - 2] === "bar" &&
      meta.keyPath[meta.keyPath.length - 1] === "baz"
    ) {
      return "special merge";
    }

    return utils.defaultMergeFunctions.mergeOthers(values);
  },
});

const x = {
  foo: { bar: { baz: 1, qux: 2 } },
  bar: { baz: 3, qux: 4 },
};
const y = {
  foo: { bar: { baz: 5, bar: { baz: 6, qux: 7 } } },
  bar: { baz: 8, qux: 9 },
};

customizedDeepmerge(x, y); // => { foo: { bar: { baz: "special merge", bar: { baz: 6, qux: 7 }, qux: 2 } }, bar: { baz: "special merge", qux: 9 }, }

declare module "../src/types" {
  interface DeepMergeMergeFunctionURItoKind<
    Ts extends Readonly<ReadonlyArray<unknown>>,
    MF extends DeepMergeMergeFunctionsURIs,
    M // This is the meta data type
  > {
    readonly KeyPathBasedMerge: Ts[number] extends number
      ? Ts[number] | string
      : DeepMergeLeaf<Ts>;
  }
}

API

See deepmerge custom API.