Skip to content
79 changes: 52 additions & 27 deletions source/merge-deep.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -79,11 +79,16 @@ Pick the rest type.

@example
```
type Rest1 = PickRestType<[]>; // => []
type Rest2 = PickRestType<[string]>; // => []
type Rest3 = PickRestType<[...number[]]>; // => number[]
type Rest4 = PickRestType<[string, ...number[]]>; // => number[]
type Rest5 = PickRestType<string[]>; // => string[]
type Rest1 = PickRestType<[]>;
//=> []
type Rest2 = PickRestType<[string]>;
//=> []
type Rest3 = PickRestType<[...number[]]>;
//=> number[]
type Rest4 = PickRestType<[string, ...number[]]>;
//=> number[]
type Rest5 = PickRestType<string[]>;
//=> string[]
```
*/
type PickRestType<Type extends UnknownArrayOrTuple> = number extends Type['length']
Expand All @@ -104,12 +109,18 @@ Omit the rest type.

@example
```
type Tuple1 = OmitRestType<[]>; // => []
type Tuple2 = OmitRestType<[string]>; // => [string]
type Tuple3 = OmitRestType<[...number[]]>; // => []
type Tuple4 = OmitRestType<[string, ...number[]]>; // => [string]
type Tuple5 = OmitRestType<[string, boolean[], ...number[]]>; // => [string, boolean[]]
type Tuple6 = OmitRestType<string[]>; // => []
type Tuple1 = OmitRestType<[]>;
//=> []
type Tuple2 = OmitRestType<[string]>;
//=> [string]
type Tuple3 = OmitRestType<[...number[]]>;
//=> []
type Tuple4 = OmitRestType<[string, ...number[]]>;
//=> [string]
type Tuple5 = OmitRestType<[string, boolean[], ...number[]]>;
//=> [string, boolean[]]
type Tuple6 = OmitRestType<string[]>;
//=> []
```
*/
type OmitRestType<Type extends UnknownArrayOrTuple, Result extends UnknownArrayOrTuple = []> = number extends Type['length']
Expand Down Expand Up @@ -238,9 +249,13 @@ type DoMergeArrayOrTuple<
Destination extends UnknownArrayOrTuple,
Source extends UnknownArrayOrTuple,
Options extends MergeDeepInternalOptions,
> = ShouldSpread<Options> extends true
? Array<Exclude<Destination, undefined>[number] | Exclude<Source, undefined>[number]>
: Source; // 'replace'
> = [Destination, Source] extends [readonly [], readonly []]
? Source extends []
? []
: readonly []
: ShouldSpread<Options> extends true
? Array<Exclude<Destination, undefined>[number] | Exclude<Source, undefined>[number]>
: Source; // 'replace'

/**
Merge two arrays recursively.
Expand Down Expand Up @@ -270,18 +285,24 @@ Merge two array/tuple recursively by selecting one of the four strategies accord
- tuple/array
- array/tuple
- array/array

Each cases are considered that the one or both are empty.
*/
type MergeDeepArrayOrTupleRecursive<
Destination extends UnknownArrayOrTuple,
Source extends UnknownArrayOrTuple,
Options extends MergeDeepInternalOptions,
> = IsBothExtends<NonEmptyTuple, Destination, Source> extends true
? MergeDeepTupleAndTupleRecursive<Destination, Source, Options>
: Destination extends NonEmptyTuple
? MergeDeepTupleAndArrayRecursive<Destination, Source, Options>
: Source extends NonEmptyTuple
? MergeDeepArrayAndTupleRecursive<Destination, Source, Options>
: MergeDeepArrayRecursive<Destination, Source, Options>;
> = Destination extends []
? Source
: Source extends []
? Destination
: IsBothExtends<NonEmptyTuple, Destination, Source> extends true
? MergeDeepTupleAndTupleRecursive<Destination, Source, Options>
: Destination extends NonEmptyTuple
? MergeDeepTupleAndArrayRecursive<Destination, Source, Options>
: Source extends NonEmptyTuple
? MergeDeepArrayAndTupleRecursive<Destination, Source, Options>
: MergeDeepArrayRecursive<Destination, Source, Options>;

/**
Merge two array/tuple according to {@link MergeDeepOptions.recurseIntoArrays recurseIntoArrays} option.
Expand Down Expand Up @@ -405,15 +426,15 @@ type Bar = {
};

type FooBar1 = MergeDeep<Foo, Bar>;
// {
// => {
// life: number;
// name: string;
// items: number[];
// a: {b: number; c: boolean; d: boolean[]};
// }

type FooBar2 = MergeDeep<Foo, Bar, {arrayMergeMode: 'spread'}>;
// {
// => {
// life: number;
// name: string;
// items: (string | number)[];
Expand All @@ -426,16 +447,20 @@ type FooBar2 = MergeDeep<Foo, Bar, {arrayMergeMode: 'spread'}>;
import type {MergeDeep} from 'type-fest';

// Merge two arrays
type ArrayMerge = MergeDeep<string[], number[]>; // => (string | number)[]
type ArrayMerge = MergeDeep<string[], number[]>;
//=> (string | number)[]

// Merge two tuples
type TupleMerge = MergeDeep<[1, 2, 3], ['a', 'b']>; // => (1 | 2 | 3 | 'a' | 'b')[]
type TupleMerge = MergeDeep<[1, 2, 3], ['a', 'b']>;
//=> (1 | 2 | 3 | 'a' | 'b')[]

// Merge an array into a tuple
type TupleArrayMerge = MergeDeep<[1, 2, 3], string[]>; // => (string | 1 | 2 | 3)[]
type TupleArrayMerge = MergeDeep<[1, 2, 3], string[]>;
//=> (string | 1 | 2 | 3)[]

// Merge a tuple into an array
type ArrayTupleMerge = MergeDeep<number[], ['a', 'b']>; // => (number | 'b' | 'a')[]
type ArrayTupleMerge = MergeDeep<number[], ['a', 'b']>;
//=> (number | 'b' | 'a')[]
```

@example
Expand Down
61 changes: 56 additions & 5 deletions test-d/merge-deep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,10 @@ expectType<{}>(mergeDeep({}, {} as const));
expectType<{}>(mergeDeep({} as const, {} as const));

// Test valid signatures for arrays/tuples.
expectType<never[]>(mergeDeep([], []));
expectType<never[]>(mergeDeep([] as const, []));
expectType<never[]>(mergeDeep([], [] as const));
expectType<never[]>(mergeDeep([] as const, [] as const));
expectType<[]>(mergeDeep([], []));
expectType<[]>(mergeDeep([] as const, []));
expectType<readonly []>(mergeDeep([], [] as const));
expectType<readonly []>(mergeDeep([] as const, [] as const));

// Test invalid signatures.
expectType<never>(mergeDeep({}, []));
Expand Down Expand Up @@ -56,7 +56,7 @@ expectType<Array<string | 42>>(mergeDeep(['life'], [42] as const, {arrayMergeMod
expectType<Array<'life' | 42>>(mergeDeep(['life'] as const, [42] as const, {arrayMergeMode: 'replace'}));

// Should merge tuples with union
expectType<Array<number | string | boolean>>(mergeDeep(['life', true], [42], {arrayMergeMode: 'spread'}));
expectType<Array<number | string | true>>(mergeDeep(['life', true], [42], {arrayMergeMode: 'spread'}));
expectType<Array<number | string | true>>(mergeDeep(['life'], [42, true], {arrayMergeMode: 'spread'}));

// Should not deep merge classes
Expand Down Expand Up @@ -325,3 +325,54 @@ type OptionalWithUndefined = {a: string | undefined; b?: number; c?: boolean | u

expectType<OptionalWithUndefined>({} as MergeDeep<NotOptional, OptionalWithUndefined>);
expectType<NotOptional>({} as MergeDeep<OptionalWithUndefined, NotOptional>);

// Test for https://github.com/sindresorhus/type-fest/issues/1200
type EmptyFoo = {foo: []};
type NotEmptyTupleFoo = {foo: [0, 1]};
type NotEmptyArrayFoo = {foo: number[]};
expectType<NotEmptyTupleFoo>({} as MergeDeep<EmptyFoo, NotEmptyTupleFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyTupleFoo>({} as MergeDeep<NotEmptyTupleFoo, EmptyFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyArrayFoo>({} as MergeDeep<EmptyFoo, NotEmptyArrayFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyArrayFoo>({} as MergeDeep<NotEmptyArrayFoo, EmptyFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expectType<NotEmptyArrayFoo>({} as MergeDeep<NotEmptyArrayFoo, EmptyFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyArrayFoo>({} as MergeDeep<NotEmptyArrayFoo, EmptyFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyTupleFoo>({} as MergeDeep<EmptyFoo, NotEmptyTupleFoo, {recurseIntoArrays: true; arrayMergeMode: 'spread'}>);
expectType<NotEmptyTupleFoo>({} as MergeDeep<NotEmptyTupleFoo, EmptyFoo, {recurseIntoArrays: true; arrayMergeMode: 'spread'}>);
expectType<NotEmptyArrayFoo>({} as MergeDeep<EmptyFoo, NotEmptyArrayFoo, {recurseIntoArrays: true; arrayMergeMode: 'spread'}>);
expectType<NotEmptyArrayFoo>({} as MergeDeep<NotEmptyArrayFoo, EmptyFoo, {recurseIntoArrays: true; arrayMergeMode: 'spread'}>);


// Test for https://github.com/sindresorhus/type-fest/issues/1200
type EmptyReadonlyFoo = {readonly foo: []};
type NotEmptyReadonlyTupleFoo = {readonly foo: [0, 1]};
type NotEmptyReadonlyArrayFoo = {readonly foo: number[]};
expectType<NotEmptyReadonlyTupleFoo>({} as MergeDeep<EmptyReadonlyFoo, NotEmptyReadonlyTupleFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyReadonlyTupleFoo>({} as MergeDeep<NotEmptyReadonlyTupleFoo, EmptyReadonlyFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyReadonlyArrayFoo>({} as MergeDeep<EmptyReadonlyFoo, NotEmptyReadonlyArrayFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyReadonlyArrayFoo>({} as MergeDeep<NotEmptyReadonlyArrayFoo, EmptyReadonlyFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);

// Test for https://github.com/sindresorhus/type-fest/issues/1200
type EmptyOptionalFoo = {foo?: []};
type NotEmptyOptionalTupleFoo = {foo?: [0, 1]};
type NotEmptyOptionalArrayFoo = {foo?: number[]};
expectType<NotEmptyOptionalTupleFoo>({} as MergeDeep<EmptyOptionalFoo, NotEmptyOptionalTupleFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyOptionalTupleFoo>({} as MergeDeep<NotEmptyOptionalTupleFoo, EmptyOptionalFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyOptionalArrayFoo>({} as MergeDeep<EmptyOptionalFoo, NotEmptyOptionalArrayFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyOptionalArrayFoo>({} as MergeDeep<NotEmptyOptionalArrayFoo, EmptyOptionalFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);

// Test for https://github.com/sindresorhus/type-fest/issues/1200
type EmptyOptionalReadonlyFoo = {readonly foo?: []};
type NotEmptyOptionalReadonlyTupleFoo = {readonly foo?: [0, 1]};
type NotEmptyOptionalReadonlyArrayFoo = {readonly foo?: number[]};
expectType<NotEmptyOptionalReadonlyTupleFoo>({} as MergeDeep<EmptyOptionalReadonlyFoo, NotEmptyOptionalReadonlyTupleFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyOptionalReadonlyTupleFoo>({} as MergeDeep<NotEmptyOptionalReadonlyTupleFoo, EmptyOptionalReadonlyFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyOptionalReadonlyArrayFoo>({} as MergeDeep<EmptyOptionalReadonlyFoo, NotEmptyOptionalReadonlyArrayFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyOptionalReadonlyArrayFoo>({} as MergeDeep<NotEmptyOptionalReadonlyArrayFoo, EmptyOptionalReadonlyFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);

// Test for https://github.com/sindresorhus/type-fest/issues/1200
expectType<NotEmptyOptionalReadonlyTupleFoo>({} as MergeDeep<EmptyOptionalFoo, NotEmptyOptionalReadonlyTupleFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyOptionalTupleFoo>({} as MergeDeep<NotEmptyOptionalReadonlyTupleFoo, EmptyOptionalFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<EmptyOptionalFoo>({} as MergeDeep<EmptyOptionalReadonlyFoo, EmptyOptionalFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<EmptyOptionalReadonlyFoo>({} as MergeDeep<EmptyOptionalFoo, EmptyOptionalReadonlyFoo, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);

// Test for https://github.com/sindresorhus/type-fest/issues/1200
type EmptyFooRecursive = {foo: [0, string, {bar: []}]};
type NotEmptyTupleFooRecursive = {foo: [0, number, {bar: [0, 1]}]};
type NotEmptyArrayFooRecursive = {foo: [0, number, {bar: number[]}]};
expectType<NotEmptyTupleFooRecursive>({} as MergeDeep<EmptyFooRecursive, NotEmptyTupleFooRecursive, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<{foo: [0, string, {bar: [0, 1]}]}>({} as MergeDeep<NotEmptyTupleFooRecursive, EmptyFooRecursive, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<NotEmptyArrayFooRecursive>({} as MergeDeep<EmptyFooRecursive, NotEmptyArrayFooRecursive, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<{foo: [0, string, {bar: number[]}]}>({} as MergeDeep<NotEmptyArrayFooRecursive, EmptyFooRecursive, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
expectType<{foo: [0, string, {bar: number[]}]}>({} as MergeDeep<NotEmptyArrayFooRecursive, EmptyFooRecursive, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<{foo: [0, string, {bar: number[]}]}>({} as MergeDeep<NotEmptyArrayFooRecursive, EmptyFooRecursive, {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
// Tuple and array recursion behavior with primitive values
expectType<[string, string, ...string[]]>({} as MergeDeep<[1, 2], string[], {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<[string, string, ...string[]]>({} as MergeDeep<[1, 2], string[], {recurseIntoArrays: true; arrayMergeMode: 'spread'}>);
expectType<[1, 2, ...string[]]>({} as MergeDeep<string[], [1, 2], {recurseIntoArrays: true; arrayMergeMode: 'replace'}>);
expectType<[1, 2, ...string[]]>({} as MergeDeep<string[], [1, 2], {recurseIntoArrays: true; arrayMergeMode: 'spread'}>);