@hypernym/merge
Version:
Type-safe deep merge utility.
143 lines (140 loc) • 6.03 kB
text/typescript
/**
* Type-safe deep merge utility.
*
* Recursively combines multiple objects into a unified result while preserving strict types, offering fine-grained control over merge strategies.
*
* Provides maximum recursion depth when merging nested objects.
*
* @example
*
* ```ts
* import { merge } from '@hypernym/merge'
*
* const result = merge([{ a: 1 }, { b: 2 }, { c: 3 }]) // => { a: 1, b: 2, c: 3 }
* ```
*
* @see [Repository](https://github.com/hypernym-studio/merge)
*/
declare function merge<T extends MergeSource[], O extends MergeOptions>(sources: [...T], options?: O & MergeOptions & {
depth?: O['depth'];
}): Merge<T, O>;
type AllSources<T extends any[], K extends PropertyKey> = {
[I in keyof T]: K extends keyof T[I] ? T[I][K] : never;
}[number];
type LastSource<T extends any[], K extends PropertyKey> = T extends [
...infer R,
infer L
] ? K extends keyof L ? L[K] : LastSource<R, K> : never;
type ExtractValue<T extends MergeSource[], K extends PropertyKey> = T extends [
infer F extends MergeSource,
...infer R extends MergeSource[]
] ? K extends keyof F ? [F[K], ...ExtractValue<R, K>] : ExtractValue<R, K> : [];
type SkipRules<O extends MergeOptions> = (O['rules'] extends {
undefined: 'skip';
} ? undefined : never) | (O['rules'] extends {
null: 'skip';
} ? null : never);
type LastValue<T extends MergeSource[], K extends PropertyKey, O extends MergeOptions> = T extends [...infer R, infer L] ? K extends keyof L ? Exclude<L[K], SkipRules<O>> extends never ? LastValue<R extends MergeSource[] ? R : [], K, O> : Exclude<L[K], SkipRules<O>> : LastValue<R extends MergeSource[] ? R : [], K, O> : never;
type ArrayValue<T extends MergeSource[], K extends PropertyKey, O extends MergeOptions> = O['rules'] extends {
array: 'override';
} ? LastSource<T, K> : MergeArray<Extract<AllSources<T, K>, any[]>>[] | Exclude<LastSource<T, K>, any[]>;
type BuildTuple<L extends number, T extends any[] = []> = T['length'] extends L ? T : BuildTuple<L, [...T, any]>;
type Decrement<N extends number> = BuildTuple<N> extends [any, ...infer R] ? R['length'] : never;
type MergeFn = (...a: any[]) => any;
type MergePrimitives = string | number | boolean | bigint | symbol | Date | MergeFn;
type MergeRecord = Record<PropertyKey, any>;
type MergeSource = MergeRecord | undefined;
type MergeAllKeys<T extends any[]> = T extends [infer K, ...infer R] ? keyof K | MergeAllKeys<R extends any[] ? R : []> : never;
type MergeArray<T> = T extends any[] ? T[number] : never;
type MergeValue<T extends MergeSource[], K extends PropertyKey, O extends MergeOptions> = O['depth'] extends 0 ? LastSource<T, K> extends any[] ? ArrayValue<T, K, O> : LastSource<T, K> extends MergeRecord ? unknown : LastSource<T, K> : Extract<LastSource<T, K>, any[]> extends never ? Extract<AllSources<T, K>, any[]> extends never ? LastSource<T, K> extends MergePrimitives ? LastValue<T, K, O> : Exclude<AllSources<T, K>, SkipRules<O>> extends MergeRecord ? MergeTypes<ExtractValue<T, K>, {
rules: O['rules'];
depth: Decrement<O['depth'] extends number ? O['depth'] : 6>;
}> : LastValue<T, K, O> : LastValue<T, K, O> : ArrayValue<T, K, O>;
type MergeExpand<T> = T extends infer U ? {
[K in keyof U]: U[K];
} : never;
type MergeTypes<T extends MergeSource[], O extends MergeOptions = MergeOptions> = MergeExpand<{
[K in MergeAllKeys<T>]: Exclude<MergeValue<T, K, O>, SkipRules<O>>;
}>;
type Merge<T extends MergeSource[], O extends MergeOptions = MergeOptions> = MergeTypes<T, O>;
interface MergeRules {
/**
* Specifies the merge strategy for `array` types.
*
* - `combine` — Combines all values from all sources into a final result,
* meaning that the right sources will merge the properties with the left sources and combine their values.
* - `override` — Value from the last source overrides the others in the final result,
* meaning that the right sources will merge the properties with the left sources and overwrite their values.
*
* @example
*
* ```ts
* merge([{}], { rules: { array: 'combine' } })
* ```
*
* @default 'combine'
*/
array?: 'combine' | 'override';
/**
* Specifies the merge strategy for the `undefined` type.
*
* - `override` — Explicitly defined value from the last source overrides the others in the final result.
* - `skip` — Skips the explicitly defined value from the last source and uses the defined one.
*
* @example
*
* ```ts
* merge([{}], { rules: { undefined: 'override' } })
* ```
*
* @default 'override'
*/
undefined?: 'override' | 'skip';
/**
* Specifies the merge strategy for the `null` type.
*
* - `override` — Explicitly defined value from the last source overrides the others in the final result.
* - `skip` — Skips the explicitly defined value from the last source and uses the defined one.
*
* @example
*
* ```ts
* merge([{}], { rules: { null: 'override' } })
* ```
*
* @default 'override'
*/
null?: 'override' | 'skip';
}
interface MergeOptions {
/**
* Defines how merging behaves for the specified types.
*
* @example
*
* ```ts
* merge([{}], { rules: {} })
* ```
*
* @default undefined
*/
rules?: MergeRules;
/**
* Specifies the maximum recursion depth when merging nested objects.
*
* The depth counter is a safeguard that limits recursion, improving compiler performance, and prevents possible infinite type instantiation issues during type-checking.
*
* In most cases, you won't need to change this.
*
* @example
*
* ```ts
* merge([{}], { depth: 9 })
* ```
*
* @default 6
*/
depth?: number;
}
export { merge };
export type { Merge, MergeAllKeys, MergeArray, MergeExpand, MergeFn, MergeOptions, MergePrimitives, MergeRecord, MergeRules, MergeSource, MergeTypes, MergeValue };