From b317e606af2c179594e04af66edbb46cc777b9c6 Mon Sep 17 00:00:00 2001 From: semantic-release-bot Date: Mon, 22 Nov 2021 12:15:00 +0000 Subject: [PATCH] chore(release): 1.1.6 [skip ci] ## [1.1.6](https://github.com/RebeccaStevens/deepmerge-ts/compare/v1.1.5...v1.1.6) (2021-11-22) ### Performance Improvements * convert recursive types to tail-recursive versions ([#15](https://github.com/RebeccaStevens/deepmerge-ts/issues/15)) ([4401ac2](https://github.com/RebeccaStevens/deepmerge-ts/commit/4401ac2d1651093ab855d3d4bdf6c9628c0767ab)) --- CHANGELOG.md | 7 ++ deno-dist/CHANGELOG.md | 7 ++ deno-dist/deepmerge.ts | 7 +- deno-dist/docs/API.md | 73 ++++++++++++++ deno-dist/docs/deepmergeCustom.md | 94 +++++++++++++++++ deno-dist/types/defaults.ts | 76 +++++++------- deno-dist/types/index.ts | 1 - deno-dist/types/options.ts | 4 +- deno-dist/types/utils.ts | 161 ++++++++++++++++++++---------- deno-dist/utils.ts | 8 +- 10 files changed, 330 insertions(+), 108 deletions(-) create mode 100644 deno-dist/docs/API.md create mode 100644 deno-dist/docs/deepmergeCustom.md diff --git a/CHANGELOG.md b/CHANGELOG.md index eb6841dd..b9515ec4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog All notable changes to this project will be documented in this file. Dates are displayed in UTC. +## [1.1.6](https://github.com/RebeccaStevens/deepmerge-ts/compare/v1.1.5...v1.1.6) (2021-11-22) + + +### Performance Improvements + +* convert recursive types to tail-recursive versions ([#15](https://github.com/RebeccaStevens/deepmerge-ts/issues/15)) ([4401ac2](https://github.com/RebeccaStevens/deepmerge-ts/commit/4401ac2d1651093ab855d3d4bdf6c9628c0767ab)) + ## [1.1.5](https://github.com/RebeccaStevens/deepmerge-ts/compare/v1.1.4...v1.1.5) (2021-10-18) diff --git a/deno-dist/CHANGELOG.md b/deno-dist/CHANGELOG.md index 3b742867..eb6841dd 100644 --- a/deno-dist/CHANGELOG.md +++ b/deno-dist/CHANGELOG.md @@ -1,6 +1,13 @@ # Changelog All notable changes to this project will be documented in this file. Dates are displayed in UTC. +## [1.1.5](https://github.com/RebeccaStevens/deepmerge-ts/compare/v1.1.4...v1.1.5) (2021-10-18) + + +### Bug Fixes + +* **deno:** deno release fixup ([4b8ca98](https://github.com/RebeccaStevens/deepmerge-ts/commit/4b8ca9868de78228244b099dc2040c4cb16a649d)) + ## [1.1.4](https://github.com/RebeccaStevens/deepmerge-ts/compare/v1.1.3...v1.1.4) (2021-10-18) ## [1.1.3](https://github.com/RebeccaStevens/deepmerge-ts/compare/v1.1.2...v1.1.3) (2021-09-21) diff --git a/deno-dist/deepmerge.ts b/deno-dist/deepmerge.ts index fc9964fb..9cd4acbb 100644 --- a/deno-dist/deepmerge.ts +++ b/deno-dist/deepmerge.ts @@ -9,7 +9,6 @@ import type { DeepMergeSetsDefaultHKT, DeepMergeMergeFunctionUtils, GetDeepMergeMergeFunctionsURIs, - RecordProperty, } from "./types/index.ts"; import { getIterableOfIterables, @@ -137,7 +136,7 @@ function mergeUnknowns< switch (type) { case ObjectType.RECORD: return utils.mergeFunctions.mergeRecords( - values as ReadonlyArray>>, + values as ReadonlyArray>>, utils ) as DeepMergeHKT; @@ -173,11 +172,11 @@ function mergeUnknowns< * @param values - The records. */ function mergeRecords< - Ts extends ReadonlyArray>, + Ts extends ReadonlyArray>, U extends DeepMergeMergeFunctionUtils, MF extends DeepMergeMergeFunctionsURIs >(values: Ts, utils: U) { - const result: Record = {}; + const result: Record = {}; /* eslint-disable functional/no-loop-statement, functional/no-conditional-statement -- using a loop here is more performant. */ diff --git a/deno-dist/docs/API.md b/deno-dist/docs/API.md new file mode 100644 index 00000000..97a845cb --- /dev/null +++ b/deno-dist/docs/API.md @@ -0,0 +1,73 @@ +# API + +## deepmerge(x, y, ...) + +Merges the given inputs together using the default configuration. + +### deepmerge(...inputs) + +Merges the array of inputs together using the default configuration. + +Note: If `inputs` isn't typed as a tuple then we cannot determine the output type. The output type will simply be `unknown`. + +## deepmergeCustom(options) + +Generate a customized deepmerge function using the given options. The returned function works just like `deepmerge` except it uses the customized configuration. + +### options + +The following options can be used to customize the deepmerge function.\ +All these options are optional. + +#### `mergeRecords` + +Type: `false | (values: Record[], utils: DeepMergeMergeFunctionUtils) => unknown` + +If false, records won't be merged. If set to a function, that function will be used to merge records. + +Note: Records are "vanilla" objects (e.g. `{ foo: "hello", bar: "world" }`). + +#### `mergeArrays` + +Type: `false | (values: unknown[][], utils: DeepMergeMergeFunctionUtils) => unknown` + +If false, arrays won't be merged. If set to a function, that function will be used to merge arrays. + +#### `mergeMaps` + +Type: `false | (values: Map[], utils: DeepMergeMergeFunctionUtils) => unknown` + +If false, maps won't be merged. If set to a function, that function will be used to merge maps. + +#### `mergeSets` + +Type: `false | (values: Set[], utils: DeepMergeMergeFunctionUtils) => unknown` + +If false, sets won't be merged. If set to a function, that function will be used to merge sets. + +#### `mergeOthers` + +Type: `(values: Set[], utils: DeepMergeMergeFunctionUtils) => unknown` + +If set to a function, that function will be used to merge everything else. + +Note: This includes merging mixed types, such as merging a map with an array. + +### DeepMergeMergeFunctionUtils + +This is a set of utility functions that are made available to your custom merge functions. + +#### `mergeFunctions` + +These are all the merge function being used to perform the deepmerge.\ +These will be the custom merge functions you gave, or the default merge functions for options you didn't customize. + +#### `defaultMergeFunctions` + +These are all the merge functions that the default, non-customize deepmerge function uses. + +#### `deepmerge` + +This is your top level customized deepmerge function. + +Note: Be careful when calling this as it is really easy to end up in an infinite loop. diff --git a/deno-dist/docs/deepmergeCustom.md b/deno-dist/docs/deepmergeCustom.md new file mode 100644 index 00000000..73d709be --- /dev/null +++ b/deno-dist/docs/deepmergeCustom.md @@ -0,0 +1,94 @@ +# 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. + +```js +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](https://www.typescriptlang.org/docs/handbook/declaration-merging.html) by declaring a module block for this library and defining the same interface. + +```ts +declare module "deepmerge-ts" { + interface DeepMergeMergeFunctionURItoKind, MF extends DeepMergeMergeFunctionsURIs> { + readonly MyCustomMergeURI: MyValue; + } +} +``` + +Here's an example of creating a custom deepmerge function that amalgamates dates into an array. + +```ts +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) => { + // 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, utils); + }, +}); + +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, + MF extends DeepMergeMergeFunctionsURIs + > { + readonly MyDeepMergeDatesURI: EveryIsDate extends true ? Ts : DeepMergeLeaf; + } +} + +type EveryIsDate> = Ts extends readonly [ + infer Head, + ...infer Rest +] + ? Head extends Date + ? EveryIsDate + : false + : true; +``` + +Note: If you want to use HKTs in your own project, not related to deepmerge-ts, we recommend checking out [fp-ts](https://gcanti.github.io/fp-ts/modules/HKT.ts.html). + +## API + +[See deepmerge custom API](./API.md#deepmergecustomoptions). diff --git a/deno-dist/types/defaults.ts b/deno-dist/types/defaults.ts index 25357ead..f233833c 100644 --- a/deno-dist/types/defaults.ts +++ b/deno-dist/types/defaults.ts @@ -1,4 +1,3 @@ -import type { RecordProperty } from "./basics.ts"; import type { DeepMergeHKT, DeepMergeLeafURI, @@ -77,57 +76,42 @@ type DeepMergeRecordsDefaultHKTInternalProps< MF extends DeepMergeMergeFunctionsURIs > = { [K in OptionalKeysOf]?: DeepMergeHKT< - FilterOutNever< - DeepMergeRecordsDefaultHKTInternalPropsToMerge< - DeepMergeRecordsDefaultHKTInternalPropValue - > - >, + DeepMergeRecordsDefaultHKTInternalPropValue, MF >; } & { [K in RequiredKeysOf]: DeepMergeHKT< - FilterOutNever< - DeepMergeRecordsDefaultHKTInternalPropsToMerge< - DeepMergeRecordsDefaultHKTInternalPropValue - > - >, + DeepMergeRecordsDefaultHKTInternalPropValue, MF >; }; /** - * Get the properties to merge. + * Get the value of the property. */ -type DeepMergeRecordsDefaultHKTInternalPropsToMerge< - Ts extends readonly [unknown, unknown] -> = Ts extends readonly [infer First, infer Second] - ? IsNever extends true - ? Second extends readonly [unknown, unknown] - ? DeepMergeRecordsDefaultHKTInternalPropsToMerge - : Second extends readonly [unknown] - ? Second - : [] - : Second extends readonly [unknown, unknown] - ? [First, ...DeepMergeRecordsDefaultHKTInternalPropsToMerge] - : Second extends readonly [unknown] - ? [First, Second[0]] - : [] - : never; +type DeepMergeRecordsDefaultHKTInternalPropValue< + Ts extends readonly [unknown, ...unknown[]], + K extends PropertyKey +> = FilterOutNever< + DeepMergeRecordsDefaultHKTInternalPropValueHelper +>; /** - * Get the value of the property. + * Tail-recursive helper type for DeepMergeRecordsDefaultHKTInternalPropValue. */ -type DeepMergeRecordsDefaultHKTInternalPropValue< +type DeepMergeRecordsDefaultHKTInternalPropValueHelper< Ts extends readonly [unknown, ...unknown[]], - K extends RecordProperty + K extends PropertyKey, + Acc extends ReadonlyArray > = Ts extends readonly [infer Head, ...infer Rest] - ? Head extends Record + ? Head extends Record ? Rest extends readonly [unknown, ...unknown[]] - ? [ - ValueOfKey, - DeepMergeRecordsDefaultHKTInternalPropValue - ] - : [ValueOfKey] + ? DeepMergeRecordsDefaultHKTInternalPropValueHelper< + Rest, + K, + [...Acc, ValueOfKey] + > + : [...Acc, ValueOfKey] : never : never; @@ -137,11 +121,23 @@ type DeepMergeRecordsDefaultHKTInternalPropValue< export type DeepMergeArraysDefaultHKT< Ts extends ReadonlyArray, MF extends DeepMergeMergeFunctionsURIs -> = Ts extends [infer Head, ...infer Rest] +> = DeepMergeArraysDefaultHKTHelper; + +/** + * Tail-recursive helper type for DeepMergeArraysDefaultHKT. + */ +type DeepMergeArraysDefaultHKTHelper< + Ts extends ReadonlyArray, + MF extends DeepMergeMergeFunctionsURIs, + Acc extends ReadonlyArray +> = Ts extends readonly [infer Head, ...infer Rest] ? Head extends ReadonlyArray - ? Rest extends [ReadonlyArray, ...ReadonlyArray] - ? [...Head, ...DeepMergeArraysDefaultHKT] - : Head + ? Rest extends readonly [ + ReadonlyArray, + ...ReadonlyArray + ] + ? DeepMergeArraysDefaultHKTHelper + : [...Acc, ...Head] : never : never; diff --git a/deno-dist/types/index.ts b/deno-dist/types/index.ts index 28e55bd3..f6ebaff2 100644 --- a/deno-dist/types/index.ts +++ b/deno-dist/types/index.ts @@ -1,4 +1,3 @@ -export * from "./basics.ts"; export * from "./defaults.ts"; export * from "./merging.ts"; export * from "./options.ts"; diff --git a/deno-dist/types/options.ts b/deno-dist/types/options.ts index 1204b692..00c58e8a 100644 --- a/deno-dist/types/options.ts +++ b/deno-dist/types/options.ts @@ -1,7 +1,5 @@ import type { DeepMergeMergeFunctionsDefaults } from "@/deepmerge DENOIFY: DEPENDENCY UNMET (BUILTIN)"; -import type { RecordProperty } from "./basics.ts"; - /** * The options the user can pass to customize deepmerge. */ @@ -23,7 +21,7 @@ type DeepMergeOptionsFull = Readonly<{ */ type DeepMergeMergeFunctions = Readonly<{ mergeRecords: < - Ts extends ReadonlyArray>>, + Ts extends ReadonlyArray>>, U extends DeepMergeMergeFunctionUtils >( records: Ts, diff --git a/deno-dist/types/utils.ts b/deno-dist/types/utils.ts index 2ba654d6..c12e0cbb 100644 --- a/deno-dist/types/utils.ts +++ b/deno-dist/types/utils.ts @@ -1,5 +1,3 @@ -import type { RecordProperty } from "./basics.ts"; - /** * Flatten a complex type such as a union or intersection of objects into a * single object. @@ -10,8 +8,8 @@ export type FlatternAlias = { [P in keyof T]: T[P] } & {}; * Get the value of the given key in the given object. */ export type ValueOfKey< - T extends Record, - K extends RecordProperty + T extends Record, + K extends PropertyKey > = K extends keyof T ? T[K] : never; /** @@ -45,7 +43,7 @@ export type EveryIsNever> = Ts extends [ */ export type IsRecord = And< Not>, - T extends Readonly> ? true : false + T extends Readonly> ? true : false >; /** @@ -158,51 +156,80 @@ export type Not = T extends true ? false : true; * Union of the sets' values' types */ export type UnionSetValues> = - Ts extends readonly [infer Head, ...infer Rest] - ? Head extends Set - ? Rest extends ReadonlyArray - ? UnionSetValues | V1 - : V1 - : never - : never; + UnionSetValuesHelper; + +/** + * Tail-recursive helper type for UnionSetValues. + */ +type UnionSetValuesHelper< + Ts extends ReadonlyArray, + Acc +> = Ts extends readonly [infer Head, ...infer Rest] + ? Head extends Set + ? Rest extends ReadonlyArray + ? UnionSetValuesHelper + : Acc | V1 + : never + : Acc; /** * Union of the maps' values' types */ export type UnionMapKeys> = - Ts extends readonly [infer Head, ...infer Rest] - ? Head extends Map - ? Rest extends readonly [] - ? K1 - : K1 | UnionMapKeys - : never - : never; + UnionMapKeysHelper; + +/** + * Tail-recursive helper type for UnionMapKeys. + */ +type UnionMapKeysHelper< + Ts extends ReadonlyArray, + Acc +> = Ts extends readonly [infer Head, ...infer Rest] + ? Head extends Map + ? Rest extends readonly [] + ? Acc | K1 + : UnionMapKeysHelper + : never + : Acc; /** * Union of the maps' keys' types */ export type UnionMapValues> = - Ts extends readonly [infer Head, ...infer Rest] - ? Head extends Map - ? Rest extends readonly [] - ? V1 - : UnionMapValues | V1 - : never - : never; + UnionMapValuesHelper; + +/** + * Tail-recursive helper type for UnionMapValues. + */ +type UnionMapValuesHelper< + Ts extends ReadonlyArray, + Acc +> = Ts extends readonly [infer Head, ...infer Rest] + ? Head extends Map + ? Rest extends readonly [] + ? Acc | V1 + : UnionMapValuesHelper + : never + : Acc; /** * Get all the keys of the given records. */ -export type KeysOf> = Ts extends readonly [ - infer Head, - ...infer Rest -] - ? Head extends Record +export type KeysOf> = KeysOfHelper; + +/** + * Tail-recursive helper type for KeysOf. + */ +type KeysOfHelper< + Ts extends ReadonlyArray, + Acc +> = Ts extends readonly [infer Head, ...infer Rest] + ? Head extends Record ? Rest extends ReadonlyArray - ? KeysOf | keyof Head - : keyof Head + ? KeysOfHelper + : Acc | keyof Head : never - : never; + : Acc; /** * Get the keys of the type what match a certain criteria. @@ -223,13 +250,21 @@ type RequiredKeys = Exclude< * Get all the required keys on the types in the tuple. */ export type RequiredKeysOf = - Ts extends readonly [infer Head, ...infer Rest] - ? Head extends Record - ? Rest extends readonly [unknown, ...unknown[]] - ? RequiredKeys | RequiredKeysOf - : RequiredKeys - : never - : never; + RequiredKeysOfHelper; + +/** + * Tail-recursive helper type for RequiredKeysOf. + */ +type RequiredKeysOfHelper< + Ts extends readonly [unknown, ...unknown[]], + Acc +> = Ts extends readonly [infer Head, ...infer Rest] + ? Head extends Record + ? Rest extends readonly [unknown, ...unknown[]] + ? RequiredKeysOfHelper> + : Acc | RequiredKeys + : never + : Acc; /** * Get the optional keys of the type. @@ -240,25 +275,41 @@ type OptionalKeys = Exclude>; * Get all the optional keys on the types in the tuple. */ export type OptionalKeysOf = - Ts extends readonly [infer Head, ...infer Rest] - ? Head extends Record - ? Rest extends readonly [unknown, ...unknown[]] - ? OptionalKeys | OptionalKeysOf - : OptionalKeys - : never - : never; + OptionalKeysOfHelper; + +/** + * Tail-recursive helper type for OptionalKeysOf. + */ +type OptionalKeysOfHelper< + Ts extends readonly [unknown, ...unknown[]], + Acc +> = Ts extends readonly [infer Head, ...infer Rest] + ? Head extends Record + ? Rest extends readonly [unknown, ...unknown[]] + ? OptionalKeysOfHelper> + : Acc | OptionalKeys + : never + : Acc; /** * Filter out nevers from a tuple. */ export type FilterOutNever> = - T extends readonly [] - ? [] - : T extends [infer Head, ...infer Rest] - ? IsNever extends true - ? FilterOutNever - : [Head, ...FilterOutNever] - : T; + FilterOutNeverHelper; + +/** + * Tail-recursive helper type for FilterOutNever. + */ +type FilterOutNeverHelper< + T extends ReadonlyArray, + Acc extends ReadonlyArray +> = T extends readonly [] + ? Acc + : T extends readonly [infer Head, ...infer Rest] + ? IsNever extends true + ? FilterOutNeverHelper + : FilterOutNeverHelper + : T; /** * Is the type a tuple? diff --git a/deno-dist/utils.ts b/deno-dist/utils.ts index 1609af48..74632027 100644 --- a/deno-dist/utils.ts +++ b/deno-dist/utils.ts @@ -1,7 +1,5 @@ import { isPlainObject } from "https://raw.githubusercontent.com/jonschlinkert/is-plain-object/v5.0.0/is-plain-object.js"; -import type { RecordProperty } from "./types/index.ts"; - /** * The different types of objects deepmerge-ts support. */ @@ -54,8 +52,8 @@ export function getObjectType(object: unknown): ObjectType { */ export function getKeys( objects: Readonly> -): Set { - const keys = new Set(); +): Set { + const keys = new Set(); /* eslint-disable functional/no-loop-statement -- using a loop here is more efficient. */ for (const object of objects) { @@ -80,7 +78,7 @@ export function getKeys( */ export function objectHasProperty( object: object, - property: RecordProperty + property: PropertyKey ): boolean { return ( typeof object === "object" &&