Skip to content

Commit

Permalink
feat(types): add typedefs from TehShrike#211
Browse files Browse the repository at this point in the history
  • Loading branch information
Rebecca Stevens committed Nov 12, 2020
1 parent bdb2204 commit 15f9e94
Show file tree
Hide file tree
Showing 2 changed files with 304 additions and 2 deletions.
45 changes: 43 additions & 2 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -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)
}
Expand Down Expand Up @@ -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<T1, T2, O> {
options = Object.assign({
arrayMerge: defaultArrayMerge,
isMergeable: defaultIsMergeable
Expand All @@ -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<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')
}
Expand Down
261 changes: 261 additions & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -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<T1, T2, O>
: DeepMerge<T1, DeepMergeAll<[T2, ...TRest], O>, 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
? DeepMergeNonPrimitive<T1, T2, O>
: Leaf<T1, T2>;

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

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

/**
* Deep merge 2 arrays.
*/
type DeepMergeArrays<
T1,
T2,
O extends Options
> = T1 extends readonly [...infer E1]
? T2 extends readonly [...infer E2]
? GetOption<O, "arrayMerge"> extends undefined
? [...E1, ...E2]
: ReturnType<NonNullable<GetOption<O, "arrayMerge">>>
: never
: never;

/**
* 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 function that merges any 2 arrays.
*/
type ArrayMerge = (
target: Array<any>,
source: Array<any>,
options: Required<Options>
) => any;

/**
* A function that merges any 2 non-arrays objects.
*/
type ObjectMerge = (
key: string,
options: Required<Options>
) =>
| ((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];

0 comments on commit 15f9e94

Please sign in to comment.