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 13, 2020
1 parent cdd1795 commit 05ebbb2
Show file tree
Hide file tree
Showing 5 changed files with 321 additions and 37 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 { DefaultOptions, 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 FullOptions
>(target: T1, source: T2, options: 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 FullOptions>(
target: T1,
source: T2,
options: O
): DeepMerge<T1, T2, 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, O>
} else if (sourceIsArray) {
return options.arrayMerge(target, source, options)
return options.arrayMerge(target as unknown[], source as unknown[], options) as DeepMerge<
T1,
T2,
O
>
} else {
return mergeObject(target, source, options)
return mergeObject(
target as Record<Property, unknown>,
source as Record<Property, unknown>,
options
) as DeepMerge<T1, T2, 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 = DefaultOptions
>(target: T1, source: T2, options?: O): DeepMerge<T1, T2, FullOptions<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 = DefaultOptions
>(objects: [...Ts], options?: O): DeepMergeAll<Ts, 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"
61 changes: 56 additions & 5 deletions src/options.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,74 @@
import isPlainObj from "is-plain-obj"

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

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

export interface FullOptions<O extends Options = Options> extends Options {
arrayMerge: NonNullable<O["arrayMerge"]>
clone: NonNullable<O["clone"]>
customMerge?: O["customMerge"]
isMergeable: NonNullable<O["isMergeable"]>
cloneUnlessOtherwiseSpecified: <T>(value: T, options: FullOptions) => T
}

export interface DefaultOptions extends Options {
arrayMerge: typeof defaultArrayMerge
clone: true
customMerge: undefined
isMergeable: typeof defaultIsMergeable
}

/**
* 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 = (target: any[], source: any[], options: FullOptions) => any

/**
* A function that merges any 2 non-arrays values.
*/
export type ObjectMerge = (
key: any
) => ((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 readonly unknown[], T2 extends readonly unknown[]>(
target: T1,
source: 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) {
return {
arrayMerge: defaultArrayMerge,
isMergeable: defaultIsMergeable,
clone: true,
...options,
cloneUnlessOtherwiseSpecified: cloneUnlessOtherwiseSpecified,
}
} as FullOptions<O>
}
172 changes: 172 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import type { FullOptions, Options } from "./options"

/**
* 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<T1, T2, FullOptions<O>>
: DeepMerge<T1, DeepMergeAll<[T2, ...TRest], O>, FullOptions<O>>
: T1
: never

/**
* Deep merge 2 types.
*/
export type DeepMerge<T1, T2, O extends Options> = IsSame<T1, T2> extends true
? T1 | T2
: And<IsObjectOrArray<T1>, IsObjectOrArray<T2>> extends true
? DeepMergeValues<T1, T2, O>
: Leaf<T1, T2>

/**
* Deep merge 2 objects (they may be arrays).
*/
type DeepMergeValues<T1, T2, O extends Options> = And<IsArray<T1>, IsArray<T2>> extends true
? DeepMergeArrays<O>
: And<IsObject<T1>, IsObject<T2>> extends true
? DeepMergeObjects<T1, T2, O>
: Leaf<T1, T2>

/**
* Deep merge 2 non-array objects.
*/
export type DeepMergeObjects<T1, T2, O extends Options> = FlatternAlias<
// @see https://github.com/microsoft/TypeScript/issues/41448
{
-readonly [K in keyof T1]: DeepMergeObjectProps<ValueOfKey<T1, K>, ValueOfKey<T2, K>, O>
} &
{
-readonly [K in keyof T2]: DeepMergeObjectProps<ValueOfKey<T1, K>, ValueOfKey<T2, K>, O>
}
>

/**
* Deep merge 2 types that are known to be properties of an object being deeply
* merged.
*/
type DeepMergeObjectProps<T1, T2, O extends Options> = Or<
IsUndefinedOrNever<T1>,
IsUndefinedOrNever<T2>
> extends true
? Leaf<T1, T2>
: O["isMergeable"] extends undefined
? O["customMerge"] extends undefined
? DeepMerge<T1, T2, O>
: DeepMergeObjectPropsCustom<T1, T2, O>
: MaybeLeaf<T1, T2>

/**
* 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<
NonNullable<O["customMerge"]>
> extends undefined
? DeepMerge<T1, T2, O>
: undefined extends ReturnType<NonNullable<O["customMerge"]>>
? Or<IsArray<T1>, IsArray<T2>> extends true
? And<IsArray<T1>, IsArray<T2>> extends true
? DeepMergeArrays<O>
: Leaf<T1, T2>
: DeepMerge<T1, T2, O> | ReturnType<NonNullable<ReturnType<NonNullable<O["customMerge"]>>>>
: ReturnType<NonNullable<ReturnType<NonNullable<O["customMerge"]>>>>

/**
* Deep merge 2 arrays.
*/
type DeepMergeArrays<O extends Options> = ReturnType<NonNullable<FullOptions<O>["arrayMerge"]>>

/**
* Get the leaf type from 2 types that can't be merged.
*/
type Leaf<T1, T2> = IsNever<T2> extends true
? T1
: IsNever<T1> extends true
? T2
: IsUndefinedOrNever<T2> extends true
? T1
: T2

/**
* Get the leaf type from 2 types that might not be able to be merged.
*/
type MaybeLeaf<T1, T2> = Or<
Or<IsUndefinedOrNever<T1>, IsUndefinedOrNever<T2>>,
Not<And<IsObjectOrArray<T1>, IsObjectOrArray<T2>>>
> extends true
? Leaf<T1, T2>
: // 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<T> = {} & { [P in keyof T]: T[P] }

/**
* Get the value of the given key in the given object.
*/
type ValueOfKey<T, K> = 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, T2> = [T1] extends [T2] ? true : false

/**
* Safely test whether or not the given type is "never".
*/
type IsNever<T> = Is<T, never>

/**
* Is the given type undefined or never?
*/
type IsUndefinedOrNever<T> = Is<T, undefined>

/**
* Returns whether or not the give two types are the same.
*/
type IsSame<T1, T2> = Is<T1, T2> extends true ? Is<T2, T1> : false

/**
* Returns whether or not the given type an object (arrays are objects).
*/
type IsObjectOrArray<T> = And<Not<IsNever<T>>, T extends object ? true : false>

/**
* Returns whether or not the given type a non-array object.
*/
type IsObject<T> = And<IsObjectOrArray<T>, Not<IsArray<T>>>

/**
* Returns whether or not the given type an array.
*/
type IsArray<T> = And<Not<IsNever<T>>, T extends ReadonlyArray<any> ? true : false>

/**
* And operator for types.
*/
type And<T1 extends boolean, T2 extends boolean> = T1 extends false ? false : T2

/**
* Or operator for types.
*/
type Or<T1 extends boolean, T2 extends boolean> = T1 extends true ? true : T2

/**
* Not operator for types.
*/
type Not<T extends boolean> = T extends true ? false : true

/**
* A property that can index an object.
*/
export type Property = string | number | symbol
Loading

0 comments on commit 05ebbb2

Please sign in to comment.