Skip to content

Commit fedbc44

Browse files
authoredSep 29, 2022
Add MergeDeep type (#452)
1 parent 0c79778 commit fedbc44

File tree

5 files changed

+766
-0
lines changed

5 files changed

+766
-0
lines changed
 

‎index.d.ts

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ export type {EmptyObject, IsEmptyObject} from './source/empty-object';
99
export type {Except} from './source/except';
1010
export type {Writable} from './source/writable';
1111
export type {Merge} from './source/merge';
12+
export type {MergeDeep, MergeDeepOptions} from './source/merge-deep';
1213
export type {MergeExclusive} from './source/merge-exclusive';
1314
export type {RequireAtLeastOne} from './source/require-at-least-one';
1415
export type {RequireExactlyOne} from './source/require-exactly-one';

‎readme.md

+1
Original file line numberDiff line numberDiff line change
@@ -144,6 +144,7 @@ Click the type names for complete docs.
144144
- [`Except`](source/except.d.ts) - Create a type from an object type without certain keys. This is a stricter version of [`Omit`](https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys).
145145
- [`Writable`](source/writable.d.ts) - Create a type that strips `readonly` from all or some of an object's keys. The inverse of `Readonly<T>`.
146146
- [`Merge`](source/merge.d.ts) - Merge two types into a new type. Keys of the second type overrides keys of the first type.
147+
- [`MergeDeep`](source/merge-deep.d.ts) - Merge two objects or two arrays/tuples recursively into a new type.
147148
- [`MergeExclusive`](source/merge-exclusive.d.ts) - Create a type that has mutually exclusive keys.
148149
- [`RequireAtLeastOne`](source/require-at-least-one.d.ts) - Create a type that requires at least one of the given keys.
149150
- [`RequireExactlyOne`](source/require-exactly-one.d.ts) - Create a type that requires exactly a single key of the given keys and disallows more.

‎source/internal.d.ts

+36
Original file line numberDiff line numberDiff line change
@@ -57,3 +57,39 @@ export type UpperCaseCharacters = 'A' | 'B' | 'C' | 'D' | 'E' | 'F' | 'G' | 'H'
5757
export type WordSeparators = '-' | '_' | ' ';
5858

5959
export type StringDigit = '0' | '1' | '2' | '3' | '4' | '5' | '6' | '7' | '8' | '9';
60+
61+
/**
62+
Matches any unknown record.
63+
*/
64+
export type UnknownRecord = Record<PropertyKey, unknown>;
65+
66+
/**
67+
Matches any unknown array or tuple.
68+
*/
69+
export type UnknownArrayOrTuple = readonly [...unknown[]];
70+
71+
/**
72+
Matches any non empty tuple.
73+
*/
74+
export type NonEmptyTuple = readonly [unknown, ...unknown[]];
75+
76+
/**
77+
Returns a boolean for whether the two given types extends the base type.
78+
*/
79+
export type IsBothExtends<BaseType, FirstType, SecondType> = FirstType extends BaseType
80+
? SecondType extends BaseType
81+
? true
82+
: false
83+
: false;
84+
85+
/**
86+
Extracts the type of the first element of an array or tuple.
87+
*/
88+
export type FirstArrayElement<TArray extends UnknownArrayOrTuple> = TArray extends readonly [infer THead, ...unknown[]]
89+
? THead
90+
: never;
91+
92+
/**
93+
Extracts the type of an array or tuple minus the first element.
94+
*/
95+
export type ArrayTail<TArray extends UnknownArrayOrTuple> = TArray extends readonly [unknown, ...infer TTail] ? TTail : [];

‎source/merge-deep.d.ts

+470
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,470 @@
1+
import type {ConditionalSimplifyDeep} from './conditional-simplify';
2+
import type {OmitIndexSignature} from './omit-index-signature';
3+
import type {PickIndexSignature} from './pick-index-signature';
4+
import type {EnforceOptional} from './enforce-optional';
5+
import type {Merge} from './merge';
6+
import type {
7+
ArrayTail,
8+
FirstArrayElement,
9+
IsBothExtends,
10+
NonEmptyTuple,
11+
UnknownArrayOrTuple,
12+
UnknownRecord,
13+
} from './internal';
14+
15+
/**
16+
Deeply smplifies an object excluding iterables and functions. Used internally to improve the UX and accept both interfaces and type aliases as inputs.
17+
*/
18+
type SimplifyDeep<Type> = ConditionalSimplifyDeep<Type, Function | Iterable<unknown>, object>;
19+
20+
/**
21+
Try to merge two record properties or return the source property value, preserving `undefined` properties values in both cases.
22+
*/
23+
type MergeDeepRecordProperty<
24+
Destination,
25+
Source,
26+
Options extends MergeDeepInternalOptions,
27+
> = undefined extends Source
28+
? MergeDeepOrReturn<Source, Exclude<Destination, undefined>, Exclude<Source, undefined>, Options> | undefined
29+
: MergeDeepOrReturn<Source, Destination, Source, Options>;
30+
31+
/**
32+
Walk through the union of the keys of the two objects and test in which object the properties are defined.
33+
- If the source does not contain the key, the value of the destination is returned.
34+
- If the source contains the key and the destination does not contain the key, the value of the source is returned.
35+
- If both contain the key, try to merge according to the chosen {@link MergeDeepOptions options} or return the source if unable to merge.
36+
*/
37+
type DoMergeDeepRecord<
38+
Destination extends UnknownRecord,
39+
Source extends UnknownRecord,
40+
Options extends MergeDeepInternalOptions,
41+
> = EnforceOptional<{
42+
[Key in keyof Destination | keyof Source]: Key extends keyof Source
43+
? Key extends keyof Destination
44+
? MergeDeepRecordProperty<Destination[Key], Source[Key], Options>
45+
: Source[Key]
46+
: Key extends keyof Destination
47+
? Destination[Key]
48+
: never;
49+
}>;
50+
51+
/**
52+
Wrapper around {@link DoMergeDeepRecord} which preserves index signatures.
53+
*/
54+
type MergeDeepRecord<
55+
Destination extends UnknownRecord,
56+
Source extends UnknownRecord,
57+
Options extends MergeDeepInternalOptions,
58+
> = DoMergeDeepRecord<OmitIndexSignature<Destination>, OmitIndexSignature<Source>, Options>
59+
& Merge<PickIndexSignature<Destination>, PickIndexSignature<Source>>;
60+
61+
/**
62+
Pick the rest type.
63+
64+
@example
65+
```
66+
type Rest1 = PickRestType<[]>; // => []
67+
type Rest2 = PickRestType<[string]>; // => []
68+
type Rest3 = PickRestType<[...number[]]>; // => number[]
69+
type Rest4 = PickRestType<[string, ...number[]]>; // => number[]
70+
type Rest5 = PickRestType<string[]>; // => string[]
71+
```
72+
*/
73+
type PickRestType<Type extends UnknownArrayOrTuple> = number extends Type['length']
74+
? ArrayTail<Type> extends [] ? Type : PickRestType<ArrayTail<Type>>
75+
: [];
76+
77+
/**
78+
Omit the rest type.
79+
80+
@example
81+
```
82+
type Tuple1 = OmitRestType<[]>; // => []
83+
type Tuple2 = OmitRestType<[string]>; // => [string]
84+
type Tuple3 = OmitRestType<[...number[]]>; // => []
85+
type Tuple4 = OmitRestType<[string, ...number[]]>; // => [string]
86+
type Tuple5 = OmitRestType<[string, boolean[], ...number[]]>; // => [string, boolean[]]
87+
type Tuple6 = OmitRestType<string[]>; // => []
88+
```
89+
*/
90+
type OmitRestType<Type extends UnknownArrayOrTuple, Result extends UnknownArrayOrTuple = []> = number extends Type['length']
91+
? ArrayTail<Type> extends [] ? Result : OmitRestType<ArrayTail<Type>, [...Result, FirstArrayElement<Type>]>
92+
: Type;
93+
94+
// Utility to avoid picking two times the type.
95+
type TypeNumberOrType<Type extends UnknownArrayOrTuple> = Type[number] extends never ? Type : Type[number];
96+
97+
// Pick the rest type (array) and try to get the intrinsic type or return the provided type.
98+
type PickRestTypeFlat<Type extends UnknownArrayOrTuple> = TypeNumberOrType<PickRestType<Type>>;
99+
100+
/**
101+
Try to merge two array/tuple elements or return the source element if the end of the destination is reached or vis-versa.
102+
*/
103+
type MergeDeepArrayOrTupleElements<
104+
Destination,
105+
Source,
106+
Options extends MergeDeepInternalOptions,
107+
> = Source extends []
108+
? Destination
109+
: Destination extends []
110+
? Source
111+
: MergeDeepOrReturn<Source, Destination, Source, Options>;
112+
113+
/**
114+
Merge two tuples recursively.
115+
*/
116+
type DoMergeDeepTupleAndTupleRecursive<
117+
Destination extends UnknownArrayOrTuple,
118+
Source extends UnknownArrayOrTuple,
119+
DestinationRestType,
120+
SourceRestType,
121+
Options extends MergeDeepInternalOptions,
122+
> = Destination extends []
123+
? Source extends []
124+
? []
125+
: MergeArrayTypeAndTuple<DestinationRestType, Source, Options>
126+
: Source extends []
127+
? MergeTupleAndArrayType<Destination, SourceRestType, Options>
128+
: [
129+
MergeDeepArrayOrTupleElements<FirstArrayElement<Destination>, FirstArrayElement<Source>, Options>,
130+
...DoMergeDeepTupleAndTupleRecursive<ArrayTail<Destination>, ArrayTail<Source>, DestinationRestType, SourceRestType, Options>,
131+
];
132+
133+
/**
134+
Merge two tuples recursively taking into account a possible rest element.
135+
*/
136+
type MergeDeepTupleAndTupleRecursive<
137+
Destination extends UnknownArrayOrTuple,
138+
Source extends UnknownArrayOrTuple,
139+
Options extends MergeDeepInternalOptions,
140+
> = [
141+
...DoMergeDeepTupleAndTupleRecursive<OmitRestType<Destination>, OmitRestType<Source>, PickRestTypeFlat<Destination>, PickRestTypeFlat<Source>, Options>,
142+
...MergeDeepArrayOrTupleElements<PickRestType<Destination>, PickRestType<Source>, Options>,
143+
];
144+
145+
/**
146+
Merge an array type with a tuple recursively.
147+
*/
148+
type MergeTupleAndArrayType<
149+
Tuple extends UnknownArrayOrTuple,
150+
ArrayType,
151+
Options extends MergeDeepInternalOptions,
152+
> = Tuple extends []
153+
? Tuple
154+
: [
155+
MergeDeepArrayOrTupleElements<FirstArrayElement<Tuple>, ArrayType, Options>,
156+
...MergeTupleAndArrayType<ArrayTail<Tuple>, ArrayType, Options>,
157+
];
158+
159+
/**
160+
Merge an array into a tuple recursively taking into account a possible rest element.
161+
*/
162+
type MergeDeepTupleAndArrayRecursive<
163+
Destination extends UnknownArrayOrTuple,
164+
Source extends UnknownArrayOrTuple,
165+
Options extends MergeDeepInternalOptions,
166+
> = [
167+
...MergeTupleAndArrayType<OmitRestType<Destination>, Source[number], Options>,
168+
...MergeDeepArrayOrTupleElements<PickRestType<Destination>, PickRestType<Source>, Options>,
169+
];
170+
171+
/**
172+
Merge a tuple with an array type recursively.
173+
*/
174+
type MergeArrayTypeAndTuple<
175+
ArrayType,
176+
Tuple extends UnknownArrayOrTuple,
177+
Options extends MergeDeepInternalOptions,
178+
> = Tuple extends []
179+
? Tuple
180+
: [
181+
MergeDeepArrayOrTupleElements<ArrayType, FirstArrayElement<Tuple>, Options>,
182+
...MergeArrayTypeAndTuple<ArrayType, ArrayTail<Tuple>, Options>,
183+
];
184+
185+
/**
186+
Merge a tuple into an array recursively taking into account a possible rest element.
187+
*/
188+
type MergeDeepArrayAndTupleRecursive<
189+
Destination extends UnknownArrayOrTuple,
190+
Source extends UnknownArrayOrTuple,
191+
Options extends MergeDeepInternalOptions,
192+
> = [
193+
...MergeArrayTypeAndTuple<Destination[number], OmitRestType<Source>, Options>,
194+
...MergeDeepArrayOrTupleElements<PickRestType<Destination>, PickRestType<Source>, Options>,
195+
];
196+
197+
/**
198+
Merge mode for array/tuple elements.
199+
*/
200+
type ArrayMergeMode = 'spread' | 'replace';
201+
202+
/**
203+
Test if it sould spread top-level arrays.
204+
*/
205+
type ShouldSpread<Options extends MergeDeepInternalOptions> = Options['spreadTopLevelArrays'] extends false
206+
? Options['arrayMergeMode'] extends 'spread' ? true : false
207+
: true;
208+
209+
/**
210+
Merge two arrays/tuples according to the chosen {@link MergeDeepOptions.arrayMergeMode arrayMergeMode} option.
211+
*/
212+
type DoMergeArrayOrTuple<
213+
Destination extends UnknownArrayOrTuple,
214+
Source extends UnknownArrayOrTuple,
215+
Options extends MergeDeepInternalOptions,
216+
> = ShouldSpread<Options> extends true
217+
? Array<Exclude<Destination, undefined>[number] | Exclude<Source, undefined>[number]>
218+
: Source; // 'replace'
219+
220+
/**
221+
Merge two arrays recursively.
222+
223+
If the two arrays are multi-level, we merge deeply, otherwise we merge the first level only.
224+
225+
Note: The `[number]` accessor is used to test the type of the second level.
226+
*/
227+
type MergeDeepArrayRecursive<
228+
Destination extends UnknownArrayOrTuple,
229+
Source extends UnknownArrayOrTuple,
230+
Options extends MergeDeepInternalOptions,
231+
> = Destination[number] extends UnknownArrayOrTuple
232+
? Source[number] extends UnknownArrayOrTuple
233+
? Array<MergeDeepArrayOrTupleRecursive<Destination[number], Source[number], Options>>
234+
: DoMergeArrayOrTuple<Destination, Source, Options>
235+
: Destination[number] extends UnknownRecord
236+
? Source[number] extends UnknownRecord
237+
? Array<SimplifyDeep<MergeDeepRecord<Destination[number], Source[number], Options>>>
238+
: DoMergeArrayOrTuple<Destination, Source, Options>
239+
: DoMergeArrayOrTuple<Destination, Source, Options>;
240+
241+
/**
242+
Merge two array/tuple recursively by selecting one of the four strategies according to the type of inputs.
243+
244+
- tuple/tuple
245+
- tuple/array
246+
- array/tuple
247+
- array/array
248+
*/
249+
type MergeDeepArrayOrTupleRecursive<
250+
Destination extends UnknownArrayOrTuple,
251+
Source extends UnknownArrayOrTuple,
252+
Options extends MergeDeepInternalOptions,
253+
> = IsBothExtends<NonEmptyTuple, Destination, Source> extends true
254+
? MergeDeepTupleAndTupleRecursive<Destination, Source, Options>
255+
: Destination extends NonEmptyTuple
256+
? MergeDeepTupleAndArrayRecursive<Destination, Source, Options>
257+
: Source extends NonEmptyTuple
258+
? MergeDeepArrayAndTupleRecursive<Destination, Source, Options>
259+
: MergeDeepArrayRecursive<Destination, Source, Options>;
260+
261+
/**
262+
Merge two array/tuple according to {@link MergeDeepOptions.recurseIntoArrays recurseIntoArrays} option.
263+
*/
264+
type MergeDeepArrayOrTuple<
265+
Destination extends UnknownArrayOrTuple,
266+
Source extends UnknownArrayOrTuple,
267+
Options extends MergeDeepInternalOptions,
268+
> = Options['recurseIntoArrays'] extends true
269+
? MergeDeepArrayOrTupleRecursive<Destination, Source, Options>
270+
: DoMergeArrayOrTuple<Destination, Source, Options>;
271+
272+
/**
273+
Try to merge two objects or two arrays/tuples recursively into a new type or return the default value.
274+
*/
275+
type MergeDeepOrReturn<
276+
DefaultType,
277+
Destination,
278+
Source,
279+
Options extends MergeDeepInternalOptions,
280+
> = SimplifyDeep<[undefined] extends [Destination | Source]
281+
? DefaultType
282+
: Destination extends UnknownRecord
283+
? Source extends UnknownRecord
284+
? MergeDeepRecord<Destination, Source, Options>
285+
: DefaultType
286+
: Destination extends UnknownArrayOrTuple
287+
? Source extends UnknownArrayOrTuple
288+
? MergeDeepArrayOrTuple<Destination, Source, Merge<Options, {spreadTopLevelArrays: false}>>
289+
: DefaultType
290+
: DefaultType>;
291+
292+
/**
293+
MergeDeep options.
294+
295+
@see {@link MergeDeep}
296+
*/
297+
export type MergeDeepOptions = {
298+
/**
299+
Merge mode for array and tuple.
300+
301+
When we walk through the properties of the objects and the same key is found and both are array or tuple, a merge mode must be chosen:
302+
- `replace`: Replaces the destination value by the source value. This is the default mode.
303+
- `spread`: Spreads the destination and the source values.
304+
305+
See {@link MergeDeep} for usages and examples.
306+
307+
Note: Top-level arrays and tuples are always spread.
308+
309+
@default 'spread'
310+
*/
311+
arrayMergeMode?: ArrayMergeMode;
312+
313+
/**
314+
Whether to affect the individual elements of arrays and tuples.
315+
316+
If this option is set to `true` the following rules are applied:
317+
- If the source does not contain the key, the value of the destination is returned.
318+
- If the source contains the key and the destination does not contain the key, the value of the source is returned.
319+
- If both contain the key, try to merge according to the chosen {@link MergeDeepOptions.arrayMergeMode arrayMergeMode} or return the source if unable to merge.
320+
321+
@default false
322+
*/
323+
recurseIntoArrays?: boolean;
324+
};
325+
326+
/**
327+
Internal options.
328+
*/
329+
type MergeDeepInternalOptions = Merge<MergeDeepOptions, {spreadTopLevelArrays?: boolean}>;
330+
331+
/**
332+
Merge default and internal options with user provided options.
333+
*/
334+
type DefaultMergeDeepOptions<Options extends MergeDeepOptions> = Merge<{
335+
arrayMergeMode: 'replace';
336+
recurseIntoArrays: false;
337+
spreadTopLevelArrays: true;
338+
}, Options>;
339+
340+
/**
341+
This utility selects the correct entry point with the corresponding default options. This avoids re-merging the options at each iteration.
342+
*/
343+
type MergeDeepWithDefaultOptions<Destination, Source, Options extends MergeDeepOptions> = SimplifyDeep<
344+
[undefined] extends [Destination | Source]
345+
? never
346+
: Destination extends UnknownRecord
347+
? Source extends UnknownRecord
348+
? MergeDeepRecord<Destination, Source, DefaultMergeDeepOptions<Options>>
349+
: never
350+
: Destination extends UnknownArrayOrTuple
351+
? Source extends UnknownArrayOrTuple
352+
? MergeDeepArrayOrTuple<Destination, Source, DefaultMergeDeepOptions<Options>>
353+
: never
354+
: never
355+
>;
356+
357+
/**
358+
Merge two objects or two arrays/tuples recursively into a new type.
359+
360+
- Properties that only exist in one object are copied into the new object.
361+
- Properties that exist in both objects are merged if possible or replaced by the one of the source if not.
362+
- Top-level arrays and tuples are always spread.
363+
- By default, inner arrays and tuples are replaced. See {@link MergeDeepOptions.arrayMergeMode arrayMergeMode} option to change this behaviour.
364+
- By default, individual array/tuple elements are not affected. See {@link MergeDeepOptions.recurseIntoArrays recurseIntoArrays} option to change this behaviour.
365+
366+
@example
367+
```
368+
import type {MergeDeep} from 'type-fest';
369+
370+
type Foo = {
371+
life: number;
372+
items: string[];
373+
a: {b: string; c: boolean; d: number[]};
374+
};
375+
376+
interface Bar {
377+
name: string;
378+
items: number[];
379+
a: {b: number; d: boolean[]};
380+
}
381+
382+
type FooBar = MergeDeep<Foo, Bar>;
383+
// {
384+
// life: number;
385+
// name: string;
386+
// items: number[];
387+
// a: {b: number; c: boolean; d: boolean[]};
388+
// }
389+
390+
type FooBar = MergeDeep<Foo, Bar, {arrayMergeMode: 'spread'}>;
391+
// {
392+
// life: number;
393+
// name: string;
394+
// items: (string | number)[];
395+
// a: {b: number; c: boolean; d: (number | boolean)[]};
396+
// }
397+
```
398+
399+
@example
400+
```
401+
import type {MergeDeep} from 'type-fest';
402+
403+
// Merge two arrays
404+
type ArrayMerge = MergeDeep<string[], number[]>; // => (string | number)[]
405+
406+
// Merge two tuples
407+
type TupleMerge = MergeDeep<[1, 2, 3], ['a', 'b']>; // => (1 | 2 | 3 | 'a' | 'b')[]
408+
409+
// Merge an array into a tuple
410+
type TupleArrayMerge = MergeDeep<[1, 2, 3], string[]>; // => (string | 1 | 2 | 3)[]
411+
412+
// Merge a tuple into an array
413+
type ArrayTupleMerge = MergeDeep<number[], ['a', 'b']>; // => (number | 'b' | 'a')[]
414+
```
415+
416+
@example
417+
```
418+
import type {MergeDeep, MergeDeepOptions} from 'type-fest';
419+
420+
type Foo = {foo: 'foo'; fooBar: string[]};
421+
type Bar = {bar: 'bar'; fooBar: number[]};
422+
423+
type FooBar = MergeDeep<Foo, Bar>;
424+
// { foo: "foo"; bar: "bar"; fooBar: number[]}
425+
426+
type FooBarSpread = MergeDeep<Foo, Bar, {arrayMergeMode: 'spread'}>;
427+
// { foo: "foo"; bar: "bar"; fooBar: (string | number)[]}
428+
429+
type FooBarArray = MergeDeep<Foo[], Bar[]>;
430+
// (Foo | Bar)[]
431+
432+
type FooBarArrayDeep = MergeDeep<Foo[], Bar[], {recurseIntoArrays: true}>;
433+
// FooBar[]
434+
435+
type FooBarArraySpreadDeep = MergeDeep<Foo[], Bar[], {recurseIntoArrays: true; arrayMergeMode: 'spread'}>;
436+
// FooBarSpread[]
437+
438+
type FooBarTupleDeep = MergeDeep<[Foo, true, 42], [Bar, 'life'], {recurseIntoArrays: true}>;
439+
// [FooBar, 'life', 42]
440+
441+
type FooBarTupleWithArrayDeep = MergeDeep<[Foo[], true], [Bar[], 'life', 42], {recurseIntoArrays: true}>;
442+
// [FooBar[], 'life', 42]
443+
```
444+
445+
@example
446+
```
447+
import type {MergeDeep, MergeDeepOptions} from 'type-fest';
448+
449+
function mergeDeep<Destination, Source, Options extends MergeDeepOptions = {}>(
450+
destination: Destination,
451+
source: Source,
452+
options?: Options,
453+
): MergeDeep<Destination, Source, Options> {
454+
// Make your implementation ...
455+
}
456+
```
457+
458+
@experimental This type is marked as experimental because it depends on {@link ConditionalSimplifyDeep} which itself is experimental.
459+
460+
@see {@link MergeDeepOptions}
461+
462+
@category Array
463+
@category Object
464+
@category Utilities
465+
*/
466+
export type MergeDeep<Destination, Source, Options extends MergeDeepOptions = {}> = MergeDeepWithDefaultOptions<
467+
SimplifyDeep<Destination>,
468+
SimplifyDeep<Source>,
469+
Options
470+
>;

‎test-d/merge-deep.ts

+258
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,258 @@
1+
import {expectType} from 'tsd';
2+
import type {MergeDeep, MergeDeepOptions} from '../index';
3+
4+
// Test helper.
5+
declare function mergeDeep<
6+
Destination,
7+
Source,
8+
Options extends MergeDeepOptions = {},
9+
>(destination: Destination, source: Source, options?: Options): MergeDeep<Destination, Source, Options>;
10+
11+
// Test valid signatures for objects.
12+
expectType<{}>(mergeDeep({}, {}));
13+
expectType<{}>(mergeDeep({} as const, {}));
14+
expectType<{}>(mergeDeep({}, {} as const));
15+
expectType<{}>(mergeDeep({} as const, {} as const));
16+
17+
// Test valid signatures for arrays/tuples.
18+
expectType<never[]>(mergeDeep([], []));
19+
expectType<never[]>(mergeDeep([] as const, []));
20+
expectType<never[]>(mergeDeep([], [] as const));
21+
expectType<never[]>(mergeDeep([] as const, [] as const));
22+
23+
// Test invalid signatures.
24+
expectType<never>(mergeDeep({}, []));
25+
expectType<never>(mergeDeep([], {}));
26+
expectType<never>(mergeDeep(null, {}));
27+
expectType<never>(mergeDeep([], 'life'));
28+
expectType<never>(mergeDeep([], new Set()));
29+
expectType<never>(mergeDeep(new Set(), new Set()));
30+
expectType<never>(mergeDeep(undefined, undefined));
31+
expectType<never>(mergeDeep({}, undefined));
32+
expectType<never>(mergeDeep(undefined, {}));
33+
34+
// Should merge simple objects
35+
expectType<{a: string; b: number}>(mergeDeep({a: 'life'}, {b: 42}));
36+
expectType<{a: 'life'; b: number}>(mergeDeep({a: 'life'} as const, {b: 42}));
37+
expectType<{a: string; b: 42}>(mergeDeep({a: 'life'}, {b: 42} as const));
38+
expectType<{a: 'life'; b: 42}>(mergeDeep({a: 'life'} as const, {b: 42} as const));
39+
40+
// Should spread simple arrays/tuples (default mode)
41+
expectType<Array<string | number>>(mergeDeep(['life'], [42]));
42+
expectType<Array<'life' | number>>(mergeDeep(['life'] as const, [42]));
43+
expectType<Array<string | 42>>(mergeDeep(['life'], [42] as const));
44+
expectType<Array<'life' | 42>>(mergeDeep(['life'] as const, [42] as const));
45+
46+
expectType<Array<string | number>>(mergeDeep(['life'], [42], {arrayMergeMode: 'spread'}));
47+
expectType<Array<'life' | number>>(mergeDeep(['life'] as const, [42], {arrayMergeMode: 'spread'}));
48+
expectType<Array<string | 42>>(mergeDeep(['life'], [42] as const, {arrayMergeMode: 'spread'}));
49+
expectType<Array<'life' | 42>>(mergeDeep(['life'] as const, [42] as const, {arrayMergeMode: 'spread'}));
50+
51+
// Should replace simple arrays/tuples
52+
expectType<Array<string | number>>(mergeDeep(['life'], [42], {arrayMergeMode: 'replace'}));
53+
expectType<Array<'life' | number>>(mergeDeep(['life'] as const, [42], {arrayMergeMode: 'replace'}));
54+
expectType<Array<string | 42>>(mergeDeep(['life'], [42] as const, {arrayMergeMode: 'replace'}));
55+
expectType<Array<'life' | 42>>(mergeDeep(['life'] as const, [42] as const, {arrayMergeMode: 'replace'}));
56+
57+
// Should merge tuples with union
58+
expectType<Array<number | string | boolean>>(mergeDeep(['life', true], [42], {arrayMergeMode: 'spread'}));
59+
expectType<Array<number | string | boolean>>(mergeDeep(['life'], [42, true], {arrayMergeMode: 'spread'}));
60+
61+
// Sould merge simple types
62+
type Foo = {foo: string; fooBar: unknown; items: string[]};
63+
type Bar = {bar: number; fooBar: boolean; items: number[]};
64+
65+
declare const fooBar: MergeDeep<Foo, Bar>;
66+
expectType<{foo: string; bar: number; fooBar: boolean; items: number[]}>(fooBar);
67+
68+
declare const fooBarSpread: MergeDeep<Foo, Bar, {arrayMergeMode: 'spread'}>;
69+
expectType<{foo: string; bar: number; fooBar: boolean; items: Array<string | number>}>(fooBarSpread);
70+
71+
declare const fooBarReplace: MergeDeep<Foo, Bar, {arrayMergeMode: 'replace'}>;
72+
expectType<{foo: string; bar: number; fooBar: boolean; items: number[]}>(fooBarReplace);
73+
74+
// Sould merge types deep
75+
type FooDeep = {foo: Foo; fooBar: Foo; items: {foo: Foo[]; fooBar: Foo}};
76+
type BarDeep = {bar: Bar; fooBar: Bar; items: {bar: Bar[]; fooBar: Bar}};
77+
78+
declare const fooBarDeep: MergeDeep<FooDeep, BarDeep>;
79+
expectType<{
80+
foo: {
81+
foo: string;
82+
fooBar: unknown;
83+
items: string[];
84+
};
85+
bar: {
86+
bar: number;
87+
fooBar: boolean;
88+
items: number[];
89+
};
90+
fooBar: {
91+
foo: string;
92+
bar: number;
93+
fooBar: boolean;
94+
items: number[];
95+
};
96+
items: {
97+
foo: Foo[];
98+
bar: Bar[];
99+
fooBar: {
100+
foo: string;
101+
bar: number;
102+
fooBar: boolean;
103+
items: number[];
104+
};
105+
};
106+
}>(fooBarDeep);
107+
108+
// Sould merge types with index signatures deep
109+
type FooWithIndexSignature = {[x: number]: number; foo: string; items: string[]};
110+
type BarWithIndexSignature = {[x: symbol]: symbol; bar: number; items: number[]};
111+
type FooWithIndexSignatureDeep = {[x: number]: number; foo: string; fooBar: FooWithIndexSignature; items: string[]};
112+
type BarWithIndexSignatureDeep = {[x: symbol]: symbol; bar: number; fooBar: BarWithIndexSignature; items: number[]};
113+
114+
declare const fooBarWithIndexSignature: MergeDeep<FooWithIndexSignatureDeep, BarWithIndexSignatureDeep>;
115+
expectType<{
116+
[x: number]: number;
117+
[x: symbol]: symbol;
118+
foo: string;
119+
bar: number;
120+
fooBar: {
121+
[x: number]: number;
122+
[x: symbol]: symbol;
123+
foo: string;
124+
bar: number;
125+
items: number[];
126+
};
127+
items: number[];
128+
}>(fooBarWithIndexSignature);
129+
130+
// Sould merge types with optional properties deep
131+
type FooWithOptional = {foo: string; fooOptional?: string; fooBar: Foo; fooBarOptional: Foo | undefined};
132+
type BarWithOptional = {bar: number; barOptional?: number; fooBar: Bar; fooBarOptional: Bar | undefined};
133+
134+
declare const fooBarWithOptional: MergeDeep<FooWithOptional, BarWithOptional>;
135+
expectType<{
136+
foo: string;
137+
bar: number;
138+
fooOptional?: string;
139+
barOptional?: number;
140+
fooBar: {
141+
foo: string;
142+
bar: number;
143+
fooBar: boolean;
144+
items: number[];
145+
};
146+
fooBarOptional?: {
147+
foo: string;
148+
bar: number;
149+
fooBar: boolean;
150+
items: number[];
151+
};
152+
}>(fooBarWithOptional);
153+
154+
// Should merge arrays with object entries
155+
type FooArray = Foo[];
156+
type BarArray = Bar[];
157+
158+
declare const fooBarArray: MergeDeep<FooArray, BarArray>;
159+
expectType<Array<Foo | Bar>>(fooBarArray);
160+
161+
declare const fooBarArraySpread: MergeDeep<FooArray, BarArray, {arrayMergeMode: 'spread'}>;
162+
expectType<Array<Foo | Bar>>(fooBarArraySpread);
163+
164+
declare const fooBarArrayReplace: MergeDeep<FooArray, BarArray, {arrayMergeMode: 'replace'}>;
165+
expectType<Array<Foo | Bar>>(fooBarArrayReplace);
166+
167+
declare const fooBarArraySpreadRecursive: MergeDeep<FooArray, BarArray, {arrayMergeMode: 'spread'; recurseIntoArrays: true}>;
168+
expectType<Array<{
169+
foo: string;
170+
bar: number;
171+
fooBar: boolean;
172+
items: Array<string | number>;
173+
}>>(fooBarArraySpreadRecursive);
174+
175+
declare const fooBarArrayReplaceRecursive: MergeDeep<FooArray, BarArray, {arrayMergeMode: 'replace'; recurseIntoArrays: true}>;
176+
expectType<Array<{
177+
foo: string;
178+
bar: number;
179+
fooBar: boolean;
180+
items: number[];
181+
}>>(fooBarArrayReplaceRecursive);
182+
183+
declare const fooBarArraySpreadRecursiveFallback: MergeDeep<FooArray, string[], {arrayMergeMode: 'spread'; recurseIntoArrays: true}>;
184+
expectType<Array<string | Foo>>(fooBarArraySpreadRecursiveFallback);
185+
186+
declare const fooBarArrayReplaceRecursiveFallback: MergeDeep<FooArray, string[], {arrayMergeMode: 'replace'; recurseIntoArrays: true}>;
187+
expectType<Array<string | Foo>>(fooBarArrayReplaceRecursiveFallback);
188+
189+
declare const fooBarArrayDeepUnionRecursive: MergeDeep<FooArray[][], BarArray[][], {arrayMergeMode: 'spread'; recurseIntoArrays: true}>;
190+
expectType<Array<Array<Array<{
191+
foo: string;
192+
bar: number;
193+
fooBar: boolean;
194+
items: Array<string | number>;
195+
}>>>>(fooBarArrayDeepUnionRecursive);
196+
197+
declare const fooBarArrayDeepUnionRecursiveFallback: MergeDeep<FooArray[], BarArray[][], {arrayMergeMode: 'spread'; recurseIntoArrays: true}>;
198+
expectType<Array<Array<Foo | BarArray>>>(fooBarArrayDeepUnionRecursiveFallback);
199+
200+
// Should merge tuples with object entries
201+
type FooTuple = [Foo, [Foo[], 42], 'foo'];
202+
type BarTuple = [Bar, [Bar[], 'a', 'b'], 'bar', true];
203+
204+
type FooBarSpread = typeof fooBarSpread;
205+
type FooBarReplace = typeof fooBarReplace;
206+
207+
declare const fooBarTupleSpread: MergeDeep<FooTuple, BarTuple, {arrayMergeMode: 'spread'; recurseIntoArrays: true}>;
208+
expectType<[FooBarSpread, [FooBarSpread[], 'a', 'b'], 'bar', true]>(fooBarTupleSpread);
209+
210+
declare const fooBarTupleReplace: MergeDeep<FooTuple, BarTuple, {arrayMergeMode: 'replace'; recurseIntoArrays: true}>;
211+
expectType<[FooBarReplace, [FooBarReplace[], 'a', 'b'], 'bar', true]>(fooBarTupleReplace);
212+
213+
// Should merge array into tuple with object entries
214+
type FooNumberTuple = [Foo[], number[]];
215+
type BarArray2D = Bar[][];
216+
217+
declare const fooNumberTupleBarArray2DSpread: MergeDeep<FooNumberTuple, BarArray2D, {arrayMergeMode: 'spread'; recurseIntoArrays: true}>;
218+
expectType<[FooBarSpread[], Array<number | Bar>, ...BarArray2D]>(fooNumberTupleBarArray2DSpread);
219+
220+
declare const fooNumberTupleBarArray2DReplace: MergeDeep<FooNumberTuple, BarArray2D, {arrayMergeMode: 'replace'; recurseIntoArrays: true}>;
221+
expectType<[FooBarReplace[], Bar[], ...BarArray2D]>(fooNumberTupleBarArray2DReplace);
222+
223+
// Should merge tuple into array with object entries
224+
type FooArray2D = Foo[][];
225+
type BarNumberTuple = [Bar[], number[]];
226+
227+
declare const fooArray2DBarNumberTupleSpread: MergeDeep<FooArray2D, BarNumberTuple, {arrayMergeMode: 'spread'; recurseIntoArrays: true}>;
228+
expectType<[FooBarSpread[], Array<Foo | number>, ...FooArray2D]>(fooArray2DBarNumberTupleSpread);
229+
230+
declare const fooArray2DBarNumberTupleReplace: MergeDeep<FooArray2D, BarNumberTuple, {arrayMergeMode: 'replace'; recurseIntoArrays: true}>;
231+
expectType<[FooBarReplace[], number[], ...FooArray2D]>(fooArray2DBarNumberTupleReplace);
232+
233+
// Should merge array into tuple with object entries and variadic length
234+
declare const arrayIntoTupleWithVariadicSpread: MergeDeep<[number, Foo, ...Foo[]], Bar[], {arrayMergeMode: 'spread'; recurseIntoArrays: true}>;
235+
expectType<[Bar, FooBarSpread, ...FooBarSpread[]]>(arrayIntoTupleWithVariadicSpread);
236+
237+
declare const arrayIntoTupleWithVariadicReplace: MergeDeep<[number, Foo, ...Foo[]], Bar[], {arrayMergeMode: 'replace'; recurseIntoArrays: true}>;
238+
expectType<[Bar, FooBarReplace, ...FooBarReplace[]]>(arrayIntoTupleWithVariadicReplace);
239+
240+
// Should merge tuple into array with object entries and variadic length
241+
declare const tupleIntoArrayWithVariadicSpread: MergeDeep<Foo[], [number, Bar, ...Bar[]], {arrayMergeMode: 'spread'; recurseIntoArrays: true}>;
242+
expectType<[number, FooBarSpread, ...FooBarSpread[]]>(tupleIntoArrayWithVariadicSpread);
243+
244+
declare const tupleIntoArrayWithVariadicReplace: MergeDeep<Foo[], [number, Bar, ...Bar[]], {arrayMergeMode: 'replace'; recurseIntoArrays: true}>;
245+
expectType<[number, FooBarReplace, ...FooBarReplace[]]>(tupleIntoArrayWithVariadicReplace);
246+
247+
// Should merge tuple into tuple with object entries and variadic length
248+
declare const tupleIntoTupleWithVariadicSpread: MergeDeep<[number, ...Foo[]], [Bar, Bar, ...Bar[]], {arrayMergeMode: 'spread'; recurseIntoArrays: true}>;
249+
expectType<[Bar, FooBarSpread, ...FooBarSpread[]]>(tupleIntoTupleWithVariadicSpread);
250+
251+
declare const tupleIntoTupleWithVariadicSpreadReversed: MergeDeep<[Foo, ...Foo[]], [number, Bar, ...Bar[]], {arrayMergeMode: 'spread'; recurseIntoArrays: true}>;
252+
expectType<[number, FooBarSpread, ...FooBarSpread[]]>(tupleIntoTupleWithVariadicSpreadReversed);
253+
254+
declare const tupleIntoTupleWithVariadicReplace: MergeDeep<[number, ...Foo[]], [Bar, Bar, ...Bar[]], {arrayMergeMode: 'replace'; recurseIntoArrays: true}>;
255+
expectType<[Bar, FooBarReplace, ...FooBarReplace[]]>(tupleIntoTupleWithVariadicReplace);
256+
257+
declare const tupleIntoTupleWithVariadicReplaceReversed: MergeDeep<[Foo, ...Foo[]], [number, Bar, ...Bar[]], {arrayMergeMode: 'replace'; recurseIntoArrays: true}>;
258+
expectType<[number, FooBarReplace, ...FooBarReplace[]]>(tupleIntoTupleWithVariadicReplaceReversed);

0 commit comments

Comments
 (0)
Please sign in to comment.