From bdb5a74c563d552a5dbd3e446fecadd79e89a9ce Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Thu, 23 Jan 2025 14:54:44 +1300 Subject: [PATCH] fix: apply filtering to types when selecting a leaf node fixes #524 --- src/types/merging.ts | 48 ++++++++++++++++++++++++++++++---- src/types/utils.ts | 21 +++++++++++++++ tests/deepmerge-custom.test.ts | 8 +++--- tests/deepmerge.test-d.ts | 20 +++++++++++++- 4 files changed, 87 insertions(+), 10 deletions(-) diff --git a/src/types/merging.ts b/src/types/merging.ts index 86ac5f8e..8a21d1f5 100644 --- a/src/types/merging.ts +++ b/src/types/merging.ts @@ -5,7 +5,17 @@ import type { DeepMergeRecordsDefaultHKT, DeepMergeSetsDefaultHKT, } from "./defaults"; -import type { EveryIsArray, EveryIsMap, EveryIsRecord, EveryIsSet, IsNever, IsTuple } from "./utils"; +import type { + AssertType, + EveryIsArray, + EveryIsMap, + EveryIsRecord, + EveryIsSet, + IsNever, + IsTuple, + TupleTupleToTupleUnion, + UnionToTuple, +} from "./utils"; /** * Mapping of merge function URIs to the merge function type. @@ -16,7 +26,7 @@ export interface DeepMergeFunctionURItoKind< Fs extends DeepMergeFunctionsURIs, in out M, > { - readonly DeepMergeLeafURI: DeepMergeLeaf; + readonly DeepMergeLeafURI: DeepMergeLeaf; readonly DeepMergeRecordsDefaultURI: DeepMergeRecordsDefaultHKT; readonly DeepMergeArraysDefaultURI: DeepMergeArraysDefaultHKT; readonly DeepMergeSetsDefaultURI: DeepMergeSetsDefaultHKT; @@ -174,18 +184,46 @@ export type DeepMergeNoFilteringURI = "DeepMergeNoFilteringURI"; /** * Get the leaf type from many types that can't be merged. */ -export type DeepMergeLeaf> = Ts extends readonly [] +export type DeepMergeLeaf< + Ts extends ReadonlyArray, + Fs extends DeepMergeFunctionsURIs, + M, +> = Ts extends readonly [] ? never : Ts extends readonly [infer T] ? T : Ts extends readonly [...infer Rest, infer Tail] ? IsNever extends true ? Rest extends ReadonlyArray - ? DeepMergeLeaf + ? DeepMergeLeaf : never - : Tail + : DeepMergeLeafApplyFilter< + Ts, + AssertType< + ReadonlyArray, + TupleTupleToTupleUnion< + AssertType< + ReadonlyArray>, + { + [I in keyof Ts]: FilterValuesHKT, Fs, M>; + } + > + > + > + > : never; +type DeepMergeLeafApplyFilter< + Original extends ReadonlyArray, + Filtered extends ReadonlyArray, +> = Original extends readonly [...infer OriginalRest, infer OriginalTail] + ? Filtered extends readonly [...infer FilteredRest, infer FilteredTail] + ? OriginalTail extends FilteredTail + ? FilteredTail + : FilteredTail | DeepMergeLeafApplyFilter + : never + : never; + /** * The meta data deepmerge is able to provide. */ diff --git a/src/types/utils.ts b/src/types/utils.ts index 81a7b3b4..a3d917d4 100644 --- a/src/types/utils.ts +++ b/src/types/utils.ts @@ -280,3 +280,24 @@ export type UnionToTuple> = IsNever extends true ? [] : [... type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never; type LastOf = UnionToIntersection T : never> extends () => infer R ? R : never; + +/** + * Convert a tuple of tuples to a tuple of unions. + */ +export type TupleTupleToTupleUnion>> = { + [I in keyof T]: TupleToUnion; +}; + +/** + * Convert a tuple to a union. + */ +export type TupleToUnion> = T extends readonly [] + ? never + : T extends readonly [infer Head, ...infer Rest] + ? Head | TupleToUnion + : never; + +/** + * Assert that a type is of a given type. + */ +export type AssertType = T extends Expected ? T : never; diff --git a/tests/deepmerge-custom.test.ts b/tests/deepmerge-custom.test.ts index 9515e3a5..fb606092 100644 --- a/tests/deepmerge-custom.test.ts +++ b/tests/deepmerge-custom.test.ts @@ -41,13 +41,13 @@ type Entries = Array< declare module "../src/types" { interface DeepMergeFunctionURItoKind, Fs extends DeepMergeFunctionsURIs, M> { - readonly NoArrayMerge1: DeepMergeLeaf; + readonly NoArrayMerge1: DeepMergeLeaf; } } declare module "../src/types" { interface DeepMergeFunctionURItoKind, Fs extends DeepMergeFunctionsURIs, M> { - readonly MergeDates1: EveryIsDate extends true ? Ts : DeepMergeLeaf; + readonly MergeDates1: EveryIsDate extends true ? Ts : DeepMergeLeaf; } } @@ -60,7 +60,7 @@ declare module "../src/types" { Fs extends DeepMergeFunctionsURIs, M, > { - readonly KeyPathBasedMerge: Ts[number] extends number ? Ts[number] | string : DeepMergeLeaf; + readonly KeyPathBasedMerge: Ts[number] extends number ? Ts[number] | string : DeepMergeLeaf; } } @@ -76,7 +76,7 @@ declare module "../src/types" { Fs, M > - : DeepMergeLeaf; + : DeepMergeLeaf; } } diff --git a/tests/deepmerge.test-d.ts b/tests/deepmerge.test-d.ts index 18a1db16..a808d2f6 100644 --- a/tests/deepmerge.test-d.ts +++ b/tests/deepmerge.test-d.ts @@ -1,6 +1,12 @@ import { expectAssignable, expectType } from "tsd"; -import { type DeepMergeMapsDefaultHKT, type DeepMergeSetsDefaultHKT, deepmerge } from "../src"; +import { + type DeepMergeMapsDefaultHKT, + type DeepMergeNoFilteringURI, + type DeepMergeSetsDefaultHKT, + deepmerge, + deepmergeCustom, +} from "../src"; const a = { foo: "abc", @@ -252,3 +258,15 @@ expectType(test22); const r: { a?: string; b?: number; c?: boolean } = { a: "a", b: 1 }; const test23 = deepmerge(r, r); expectType<{ a?: string; b?: number; c?: boolean }>(test23); + +const s: { foo: number | undefined } = { foo: undefined }; +const test24 = deepmerge(a, s); +expectType<{ foo: string | number; baz: { quux: string[] }; garply: number }>(test24); + +const test25 = deepmergeCustom< + unknown, + { + DeepMergeFilterValuesURI: DeepMergeNoFilteringURI; + } +>({ filterValues: false })(a, s); +expectType<{ foo: number | undefined; baz: { quux: string[] }; garply: number }>(test25);