Skip to content

MergeDeep : fix producing [never] when merging empty tuples.#1330

Open
taiyakihitotsu wants to merge 9 commits into
sindresorhus:mainfrom
taiyakihitotsu:fix/issue-1200
Open

MergeDeep : fix producing [never] when merging empty tuples.#1330
taiyakihitotsu wants to merge 9 commits into
sindresorhus:mainfrom
taiyakihitotsu:fix/issue-1200

Conversation

@taiyakihitotsu

@taiyakihitotsu taiyakihitotsu commented Jan 23, 2026

Copy link
Copy Markdown
Contributor

Close #1200

Test case

I use IsEqual because this passes uncorrectly on editor (it rejects correctly with npm test though).

expectType<{f: [0]}>({} as {f: [never]})

I think this might be related to this tsd issue

Undefined behavior

type A = MergeDeep<[1, 2], string[], {recurseIntoArrays: true, arrayMergeMode: 'replace'}>
// [string, string, ...string[]]
type B = MergeDeep<[1, 2], string[], {recurseIntoArrays: true, arrayMergeMode: 'spread'}> 
// [string, string, ...string[]]
type C = MergeDeep<string[], [1, 2], {recurseIntoArrays: true, arrayMergeMode: 'replace'}> 
// [1, 2, ...string[]]
type D = MergeDeep<string[], [1, 2], {recurseIntoArrays: true, arrayMergeMode: 'spread'}> 
// [1, 2, ...string[]]

This is how it behaves in this PR, but is this according to the spec?
I couldn't find any test cases that verify this, even in the original codebase.

I think this might be somewhat related to this test case, though I'm not certain.

// Should merge array into tuple with object entries
type FooNumberTuple = [Foo[], number[]];
type BarArray2D = Bar[][];

declare const fooNumberTupleBarArray2DSpread: MergeDeep<FooNumberTuple, BarArray2D, {arrayMergeMode: 'spread'; recurseIntoArrays: true}>;
expectType<[FooBarSpread[], Array<number | Bar>, ...BarArray2D]>(fooNumberTupleBarArray2DSpread);

declare const fooNumberTupleBarArray2DReplace: MergeDeep<FooNumberTuple, BarArray2D, {arrayMergeMode: 'replace'; recurseIntoArrays: true}>;
expectType<[FooBarReplace[], Bar[], ...BarArray2D]>(fooNumberTupleBarArray2DReplace);

I'll add A to D test cases if they satisfy the spec.

@taiyakihitotsu taiyakihitotsu changed the title MergeDeep producing never[] when merging empty tuples with MergeDeep : fix producing never[] when merging empty tuples. Jan 23, 2026
@taiyakihitotsu taiyakihitotsu changed the title MergeDeep : fix producing never[] when merging empty tuples. MergeDeep : fix producing [never] when merging empty tuples. Jan 23, 2026
@som-sm

som-sm commented Jan 24, 2026

Copy link
Copy Markdown
Collaborator

I use IsEqual because this passes uncorrectly on editor (it rejects correctly with npm test though).

expectType<{f: [0]}>({} as {f: [never]})

@taiyakihitotsu As long as it works with npm test, that's fine. This is a known limitation with using expectType, you won't get errors in the editor for something like:

expectType<string | number>({} as string);

This behavior already exists in several parts of the codebase, so it's fine to use expectType for tests.

@taiyakihitotsu

Copy link
Copy Markdown
Contributor Author

@som-sm

Thanks!

I didn't have a strong conviction regarding the behavior of expectType or how it should be used in type-fest.
That’s why I had been writing the tests via IsEqual (even in my previous PRs)

Now I reverted the tests style.

Comment thread test-d/merge-deep.ts
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'}>);

Comment thread test-d/merge-deep.ts
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'}>);

@taiyakihitotsu

Copy link
Copy Markdown
Contributor Author

@sindresorhus

Thanks for reviews!
Addressed the suggested test cases.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

MergeDeep with recurseIntoArrays cannot merge an array from source into target when target array is empty

3 participants