From 04e2bf4911ab81a12d2eec7b9599fe4d7697368c Mon Sep 17 00:00:00 2001 From: Rebecca Stevens Date: Thu, 12 Nov 2020 17:05:25 +1300 Subject: [PATCH] feat(types): add typedefs from #211 closes #211 --- src/index.ts | 45 ++++++++- src/types.ts | 261 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 304 insertions(+), 2 deletions(-) create mode 100644 src/types.ts diff --git a/src/index.ts b/src/index.ts index 1db49d1..c5ebba9 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,5 +1,7 @@ import isPlainObj from 'is-plain-obj'; +import type { DeepMerge, DeepMergeAll, DefaultOptions, Options } from './types'; + function defaultIsMergeable(value) { return Array.isArray(value) || isPlainObj(value) } @@ -76,7 +78,18 @@ function mergeObject(target, source, options) { return destination } -export default function deepmerge(target, source, options) { +/** + * Deeply merge two objects. + * + * @param target The first object. + * @param source The second object. + * @param options Deep merge options. + */ +export default function deepmerge< + T1 extends object, + T2 extends object, + O extends Options = DefaultOptions + >(target: T1, source: T2, options?: O): DeepMerge { options = Object.assign({ arrayMerge: defaultArrayMerge, isMergeable: defaultIsMergeable @@ -97,7 +110,35 @@ export default function deepmerge(target, source, options) { } } -export function deepmergeAll(array, options) { +/** + * Deeply merge two or more objects. + * + * @param objects An tuple of the objects to merge. + * @param options Deep merge options. + */ +export function deepmergeAll< + Ts extends readonly [object, ...object[]], + O extends Options = DefaultOptions +>(objects: [...Ts], options?: O): DeepMergeAll; + +/** + * Deeply merge two or more objects. + * + * @param objects An array of the objects to merge. + * @param options Deep merge options. + */ +export function deepmergeAll( + objects: ReadonlyArray, + options?: Options +): object; + +/** + * Deeply merge all implementation. + */ +export function deepmergeAll( + array: ReadonlyArray, + options?: Options +): object { if (!Array.isArray(array)) { throw new Error('first argument should be an array') } diff --git a/src/types.ts b/src/types.ts new file mode 100644 index 0000000..2e74511 --- /dev/null +++ b/src/types.ts @@ -0,0 +1,261 @@ +/** + * Deep merge config options. + */ +export type Options = { + arrayMerge?: ArrayMerge; + clone?: boolean; + customMerge?: ObjectMerge; + isMergeable?: IsMergeable; +}; + +/** + * The default config options. + */ +export type DefaultOptions = { + arrayMerge: undefined; + clone: true; + customMerge: undefined; + isMergeable: undefined; +}; + +/** + * Deep merge 1 or more types given in an array. + */ +export type DeepMergeAll< + Ts extends readonly [any, ...any[]], + O extends Options +> = Ts extends readonly [infer T1, ...any[]] + ? Ts extends readonly [T1, infer T2, ...infer TRest] + ? TRest extends readonly never[] + ? DeepMerge + : DeepMerge, O> + : T1 + : never; + +/** + * Deep merge 2 types. + */ +export type DeepMerge = IsSame< + T1, + T2 +> extends true + ? T1 | T2 + : And, IsObjectOrArray> extends true + ? DeepMergeNonPrimitive + : Leaf; + +/** + * Deep merge 2 objects (they may be arrays). + */ +type DeepMergeNonPrimitive = And< + IsArray, + IsArray +> extends true + ? DeepMergeArrays + : And, IsObject> extends true + ? DeepMergeObjects + : Leaf; + +/** + * Deep merge 2 non-array objects. + */ +type DeepMergeObjects< + T1, + T2, + O extends Options +> = FlatternAlias< + // @see https://github.com/microsoft/TypeScript/issues/41448 + { + -readonly [K in keyof T1]: DeepMergeObjectProps< + ValueOfKey, + ValueOfKey, + O + >; + } & + { + -readonly [K in keyof T2]: DeepMergeObjectProps< + ValueOfKey, + ValueOfKey, + O + >; + } +>; + +/** + * Deep merge 2 types that are known to be properties of an object being deeply + * merged. + */ +type DeepMergeObjectProps = Or< + IsUndefinedOrNever, + IsUndefinedOrNever +> extends true + ? Leaf + : GetOption extends undefined + ? GetOption extends undefined + ? DeepMerge + : DeepMergeObjectPropsCustom + : MaybeLeaf; + +/** + * Deep merge 2 types that are known to be properties of an object being deeply + * merged and where a "customMerge" function has been provided. + */ +type DeepMergeObjectPropsCustom< + T1, + T2, + O extends Options +> = ReturnType>> extends undefined + ? DeepMerge + : undefined extends ReturnType>> + ? Or, IsArray> extends true + ? And, IsArray> extends true + ? DeepMergeArrays + : Leaf + : + | DeepMerge + | ReturnType< + NonNullable< + ReturnType>> + > + > + : ReturnType< + NonNullable>>> + >; + +/** + * Deep merge 2 arrays. + */ +type DeepMergeArrays< + T1, + T2, + O extends Options +> = T1 extends readonly [...infer E1] + ? T2 extends readonly [...infer E2] + ? GetOption extends undefined + ? [...E1, ...E2] + : ReturnType>> + : never + : never; + +/** + * Get the leaf type from 2 types that can't be merged. + */ +type Leaf = IsNever extends true + ? T1 + : IsNever extends true + ? T2 + : IsUndefinedOrNever extends true + ? T1 + : T2; + +/** + * Get the leaf type from 2 types that might not be able to be merged. + */ +type MaybeLeaf = Or< + Or, IsUndefinedOrNever>, + Not, IsObjectOrArray>> +> extends true + ? Leaf + // TODO: Handle case where return type of "isMergeable" is a typeguard. If it is we can do better than just "unknown". + : unknown; + +/** + * Flatten a complex type such as a union or intersection of objects into a + * single object. + */ +type FlatternAlias = {} & { [P in keyof T]: T[P] }; + +/** + * Get the value of the given key in the given object. + */ +type ValueOfKey = K extends keyof T ? T[K] : never; + +/** + * Safely test whether or not the first given types extends the second. + * + * Needed in particular for testing if a type is "never". + */ +type Is = [T1] extends [T2] ? true : false; + +/** + * Safely test whether or not the given type is "never". + */ +type IsNever = Is; + +/** + * Is the given type undefined or never? + */ +type IsUndefinedOrNever = Is; + +/** + * Returns whether or not the give two types are the same. + */ +type IsSame = Is extends true ? Is : false; + +/** + * Returns whether or not the given type an object (arrays are objects). + */ +type IsObjectOrArray = And>, T extends object ? true : false>; + +/** + * Returns whether or not the given type a non-array object. + */ +type IsObject = And, Not>>; + +/** + * Returns whether or not the given type an array. + */ +type IsArray = And< + Not>, + T extends ReadonlyArray ? true : false +>; + +/** + * And operator for types. + */ +type And = T1 extends false + ? false + : T2; + +/** + * Or operator for types. + */ +type Or = T1 extends true ? true : T2; + +/** + * Not operator for types. + */ +type Not = T extends true ? false : true; + +/** + * A function that merges any 2 arrays. + */ +type ArrayMerge = ( + target: Array, + source: Array, + options: Required +) => any; + +/** + * A function that merges any 2 non-arrays objects. + */ +type ObjectMerge = ( + key: string, + options: Required +) => + | ((target: any, source: any, options?: Options) => any) + | undefined; + +/** + * A function that determins if any non-array object is mergable. + */ +type IsMergeable = (value: any) => boolean; + +/** + * Get the type of a given config option, defaulting to the default type if it + * wasn't given. + */ +type GetOption< + O extends Options, + K extends keyof Options +> = undefined extends O[K] ? DefaultOptions[K] : O[K];