Skip to content

Commit

Permalink
MergeDeep: Fix optional key when value type is any or never (#777)
Browse files Browse the repository at this point in the history
  • Loading branch information
Emiyaaaaa committed Dec 2, 2023
1 parent 6759853 commit 609c097
Show file tree
Hide file tree
Showing 2 changed files with 42 additions and 18 deletions.
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

0 comments on commit 609c097

Please sign in to comment.