Skip to content

Commit

Permalink
add typedefs from TehShrike#211
Browse files Browse the repository at this point in the history
  • Loading branch information
Rebecca Stevens committed Nov 16, 2020
1 parent b726634 commit 9a5d906
Show file tree
Hide file tree
Showing 5 changed files with 341 additions and 40 deletions.
82 changes: 67 additions & 15 deletions src/deepmerge.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,27 @@
import { getFullOptions } from './options'
import type { ExplicitOptions, FullOptions, Options } from "./options"
import { getFullOptions } from "./options"
import type { DeepMerge, DeepMergeAll, DeepMergeObjects, Property } from "./types"
import {
cloneUnlessOtherwiseSpecified,
getKeys,
getMergeFunction,
propertyIsOnObject,
propertyIsUnsafe
} from './utils'
propertyIsUnsafe,
} from "./utils"

function mergeObject<
T1 extends Record<Property, unknown>,
T2 extends Record<Property, unknown>,
O extends Options
>(target: T1, source: T2, options: FullOptions<O>): DeepMergeObjects<T1, T2, O> {
const destination: any = {}

function mergeObject(target, source, options) {
const destination = {}
if (options.isMergeable(target)) {
getKeys(target).forEach((key) => {
destination[key] = cloneUnlessOtherwiseSpecified(target[key], options)
})
}

getKeys(source).forEach((key) => {
if (propertyIsUnsafe(target, key)) {
return
Expand All @@ -25,33 +33,77 @@ function mergeObject(target, source, options) {
destination[key] = getMergeFunction(key, options)(target[key], source[key], options)
}
})

return destination
}

export function deepmergeImpl(target, source, options) {
export function deepmergeImpl<T1 extends any, T2 extends any, O extends Options>(
target: T1,
source: T2,
options: FullOptions<O>
): DeepMerge<T1, T2, ExplicitOptions<O>> {
const sourceIsArray = Array.isArray(source)
const targetIsArray = Array.isArray(target)
const sourceAndTargetTypesMatch = sourceIsArray === targetIsArray

if (!sourceAndTargetTypesMatch) {
return cloneUnlessOtherwiseSpecified(source, options)
return cloneUnlessOtherwiseSpecified(source, options) as DeepMerge<T1, T2, ExplicitOptions<O>>
} else if (sourceIsArray) {
return options.arrayMerge(target, source, options)
return options.arrayMerge(target as unknown[], source as unknown[], options) as DeepMerge<
T1,
T2,
ExplicitOptions<O>
>
} else {
return mergeObject(target, source, options)
return mergeObject(
target as Record<Property, unknown>,
source as Record<Property, unknown>,
options
) as DeepMerge<T1, T2, ExplicitOptions<O>>
}
}

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 = {}
>(target: T1, source: T2, options?: O) {
return deepmergeImpl(target, source, getFullOptions(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 = {}
>(objects: [...Ts], options?: O): DeepMergeAll<Ts, ExplicitOptions<O>>

/**
* 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<object>, options?: Options): object

/**
* Deeply merge all implementation.
*/
export function deepmergeAll(array: ReadonlyArray<object>, options?: Options): object {
if (!Array.isArray(array)) {
throw new Error('first argument should be an array')
throw new Error("first argument should be an array")
}

return array.reduce((prev, next) =>
deepmergeImpl(prev, next, getFullOptions(options)), {}
)
return array.reduce((prev, next) => deepmergeImpl(prev, next, getFullOptions(options)), {})
}
1 change: 1 addition & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1,2 @@
export { default, deepmergeAll } from "./deepmerge"
export type { Options } from "./options"
74 changes: 66 additions & 8 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,89 @@
import isPlainObj from "is-plain-obj"

import type { Property } from "./types"
import { cloneUnlessOtherwiseSpecified } from "./utils"

function defaultIsMergeable(value) {
/**
* Deep merge options.
*/
export type Options = Partial<{
arrayMerge?: ArrayMerge
clone?: boolean
customMerge?: ObjectMerge
isMergeable?: IsMergeable
}>

/**
* Deep merge options with explicit keys.
*/
export type ExplicitOptions<O extends Options = Options> = {
[K in keyof Options]-?: undefined extends O[K] ? never : O[K]
}

/**
* Deep merge options with defaults applied.
*/
export type FullOptions<O extends Options = Options> = {
arrayMerge: O["arrayMerge"] extends undefined
? typeof defaultArrayMerge
: NonNullable<O["arrayMerge"]>
clone: O["arrayMerge"] extends undefined ? true : NonNullable<O["clone"]>
customMerge?: O["customMerge"]
isMergeable: O["arrayMerge"] extends undefined
? typeof defaultIsMergeable
: NonNullable<O["isMergeable"]>
cloneUnlessOtherwiseSpecified: <T>(value: T, options: FullOptions) => T
}

/**
* A function that determins if a type is mergable.
*/
export type IsMergeable = (value: any) => boolean

/**
* A function that merges any 2 arrays.
*/
export type ArrayMerge<T1 = any, T2 = any> = (target: T1[], source: T2[], options: FullOptions) => any

/**
* A function that merges any 2 non-arrays values.
*/
export type ObjectMerge<K = any> = (
key: K
) => ((target: any, source: any, options: FullOptions) => any) | undefined

function defaultIsMergeable(value: unknown): value is Record<Property, unknown> | Array<unknown> {
return Array.isArray(value) || isPlainObj(value)
}

function defaultArrayMerge(target, source, options) {
function defaultArrayMerge<T1 extends unknown, T2 extends unknown>(
target: readonly T1[],
source: readonly T2[],
options: FullOptions
) {
return [...target, ...source].map((element) =>
cloneUnlessOtherwiseSpecified(element, options)
)
) as T1 extends readonly [...infer E1]
? T2 extends readonly [...infer E2]
? [...E1, ...E2]
: never
: never
}

export function getFullOptions(options) {
export function getFullOptions<O extends Options>(options?: O) {
const overrides =
options === undefined
? undefined
: (Object.fromEntries(
// Filter out keys explicitly set to undefined.
Object.entries(options).filter(([key, value]) => value !== undefined)
))
) as O)

return {
arrayMerge: defaultArrayMerge,
isMergeable: defaultIsMergeable,
clone: true,
...overrides,
cloneUnlessOtherwiseSpecified
};
...overrides,
cloneUnlessOtherwiseSpecified,
} as FullOptions<O>
}
Loading

0 comments on commit 9a5d906

Please sign in to comment.