Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

MergeDeep: fixed optional when value type is any or never #777

Merged
merged 2 commits into from
Dec 2, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
30 changes: 17 additions & 13 deletions source/merge-deep.d.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import type {ConditionalSimplifyDeep} from './conditional-simplify';
import type {OmitIndexSignature} from './omit-index-signature';
import type {PickIndexSignature} from './pick-index-signature';
import type {EnforceOptional} from './enforce-optional';
import type {Merge} from './merge';
import type {
ArrayTail,
Expand Down Expand Up @@ -30,23 +29,28 @@ type MergeDeepRecordProperty<

/**
Walk through the union of the keys of the two objects and test in which object the properties are defined.
- If the source does not contain the key, the value of the destination is returned.
- If the source contains the key and the destination does not contain the key, the value of the source is returned.
- If both contain the key, try to merge according to the chosen {@link MergeDeepOptions options} or return the source if unable to merge.
Rules:
1. If the source does not contain the key, the value of the destination is returned.
2. If the source contains the key and the destination does not contain the key, the value of the source is returned.
3. If both contain the key, try to merge according to the chosen {@link MergeDeepOptions options} or return the source if unable to merge.
*/
type DoMergeDeepRecord<
Destination extends UnknownRecord,
Source extends UnknownRecord,
Options extends MergeDeepInternalOptions,
> = EnforceOptional<{
[Key in keyof Destination | keyof Source]: Key extends keyof Source
? Key extends keyof Destination
? MergeDeepRecordProperty<Destination[Key], Source[Key], Options>
: Source[Key]
: Key extends keyof Destination
? Destination[Key]
: never;
}>;
> =
// Case in rule 1: The destination contains the key but the source doesn't.
{
[Key in keyof Destination as Key extends keyof Source ? never : Key]: Destination[Key];
}
// Case in rule 2: The source contains the key but the destination doesn't.
& {
[Key in keyof Source as Key extends keyof Destination ? never : Key]: Source[Key];
}
// Case in rule 3: Both the source and the destination contain the key.
& {
[Key in keyof Source as Key extends keyof Destination ? Key : never]: MergeDeepRecordProperty<Destination[Key], Source[Key], Options>;
};

/**
Wrapper around {@link DoMergeDeepRecord} which preserves index signatures.
Expand Down
30 changes: 25 additions & 5 deletions test-d/merge-deep.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,9 @@ expectType<never>(mergeDeep(undefined, {}));

// Should merge simple objects
expectType<{a: string; b: number}>(mergeDeep({a: 'life'}, {b: 42}));
expectType<{a: 'life'; b: number}>(mergeDeep({a: 'life'} as const, {b: 42}));
expectType<{a: string; b: 42}>(mergeDeep({a: 'life'}, {b: 42} as const));
expectType<{a: 'life'; b: 42}>(mergeDeep({a: 'life'} as const, {b: 42} as const));
expectType<{readonly a: 'life'; b: number}>(mergeDeep({a: 'life'} as const, {b: 42}));
expectType<{a: string; readonly b: 42}>(mergeDeep({a: 'life'}, {b: 42} as const));
expectType<{readonly a: 'life'; readonly b: 42}>(mergeDeep({a: 'life'} as const, {b: 42} as const));

// Should spread simple arrays/tuples (default mode)
expectType<Array<string | number>>(mergeDeep(['life'], [42]));
Expand Down Expand Up @@ -157,14 +157,34 @@ expectType<{
fooBar: boolean;
items: number[];
};
fooBarOptional?: {
fooBarOptional: {
foo: string;
bar: number;
fooBar: boolean;
items: number[];
};
} | undefined;
}>(fooBarWithOptional);

// Test for optional
type FooOptional = {
string?: string;
any?: any;
never?: never;
};
type BarOptional = {
number?: number;
};
type MergedFooBar = {
string?: string;
any?: any;
never?: never;
number?: number;
};
declare const mergedFooBar: MergeDeep<FooOptional, BarOptional>;
expectType<MergedFooBar>(mergedFooBar);
declare const mergedBarFoo: MergeDeep<FooOptional, BarOptional>;
expectType<MergedFooBar>(mergedBarFoo);

// Should merge arrays with object entries
type FooArray = Foo[];
type BarArray = Bar[];
Expand Down