Skip to content

Commit

Permalink
Merge: Fix optional keys and type override (#456)
Browse files Browse the repository at this point in the history
  • Loading branch information
skarab42 authored Sep 1, 2022
1 parent 850ac04 commit 2e443e2
Show file tree
Hide file tree
Showing 4 changed files with 122 additions and 7 deletions.
47 changes: 47 additions & 0 deletions source/enforce-optional.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import type {Simplify} from './simplify';

// Returns `never` if the key is optional otherwise return the key type.
type RequiredFilter<Type, Key extends keyof Type> = undefined extends Type[Key]
? Type[Key] extends undefined
? Key
: never
: Key;

// Returns `never` if the key is required otherwise return the key type.
type OptionalFilter<Type, Key extends keyof Type> = undefined extends Type[Key]
? Type[Key] extends undefined
? never
: Key
: never;

/**
Enforce optional keys (by adding the `?` operator) for keys that have a union with `undefined`.
@example
```
import type {Merge} from 'type-fest';
type Foo = {
a: string;
b?: string;
c: undefined;
d: number | undefined;
};
type FooBar = EnforceOptional<Foo>;
// => {
// a: string;
// b?: string;
// c: undefined;
// d?: number;
// }
```
@internal
@category Object
*/
export type EnforceOptional<ObjectType> = Simplify<{
[Key in keyof ObjectType as RequiredFilter<ObjectType, Key>]: ObjectType[Key]
} & {
[Key in keyof ObjectType as OptionalFilter<ObjectType, Key>]?: Exclude<ObjectType[Key], undefined>
}>;
8 changes: 4 additions & 4 deletions source/merge.d.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import type {OmitIndexSignature} from './omit-index-signature';
import type {PickIndexSignature} from './pick-index-signature';
import type {Simplify} from './simplify';
import type {EnforceOptional} from './enforce-optional';

/**
Merge two types into a new type. Keys of the second type overrides keys of the first type.
Expand Down Expand Up @@ -36,10 +36,10 @@ export type FooBar = Merge<Foo, Bar>;
@category Object
*/
export type Merge<Destination, Source> = Simplify<{
[Key in keyof OmitIndexSignature<Destination & Source>]: Key extends keyof Source
export type Merge<Destination, Source> = EnforceOptional<{
[Key in keyof OmitIndexSignature<Destination> | keyof OmitIndexSignature<Source>]: Key extends keyof Source
? Source[Key]
: Key extends keyof Destination
? Destination[Key]
: never;
} & PickIndexSignature<Destination & Source>>;
} & PickIndexSignature<Destination> & PickIndexSignature<Source>>;
20 changes: 20 additions & 0 deletions test-d/enforce-optional.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {expectType} from 'tsd';
import type {EnforceOptional} from '../source/enforce-optional';

type Foo = {
a: string;
b?: string;
c: undefined;
d: number | undefined;
};

type EnforcedOptionalFoo = EnforceOptional<Foo>;

declare const enforcedOptionalFoo: EnforcedOptionalFoo;

expectType<{
a: string;
b?: string;
c: undefined;
d?: number;
}>(enforcedOptionalFoo);
54 changes: 51 additions & 3 deletions test-d/merge.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import {expectAssignable, expectError} from 'tsd';
import {expectError, expectType} from 'tsd';
import type {Merge} from '../index';

type Foo = {
Expand All @@ -11,7 +11,7 @@ type Bar = {
};

const ab: Merge<Foo, Bar> = {a: 1, b: 2};
expectAssignable<{a: number; b: number}>(ab);
expectType<{a: number; b: number}>(ab);

// eslint-disable-next-line @typescript-eslint/consistent-type-definitions
interface FooInterface {
Expand Down Expand Up @@ -39,7 +39,7 @@ const fooBar: FooBar = {
baz: true,
};

expectAssignable<{
expectType<{
[x: string]: unknown;
[x: number]: number;
[x: symbol]: boolean;
Expand All @@ -56,3 +56,51 @@ expectError(setFooBar({
bar: new Date(),
baz: true,
}));

// Checks that a property can be replaced by another property that is not of the same type. This issue was encountered in `MergeDeep' with the default options.
type FooDefaultOptions = {
stripUndefinedValues: false;
};

type FooOptions = Merge<FooDefaultOptions, {stripUndefinedValues: true}>;

expectType<FooOptions>({stripUndefinedValues: true});

// Test that optional keys are enforced.
type FooWithOptionaKeys = {
[x: string]: unknown;
[x: number]: unknown;
a: string;
b?: string;
c: undefined;
d: string;
e: number | undefined;
};

type BarWithOptionaKeys = {
[x: number]: number;
[x: symbol]: boolean;
a?: string;
b: string;
d?: string;
f: number | undefined;
g: undefined;
};

type FooBarWithOptionalKeys = Merge<FooWithOptionaKeys, BarWithOptionaKeys>;

declare const fooBarWithOptionalKeys: FooBarWithOptionalKeys;

// Note that `c` and `g` is not marked as optional and this is deliberate, as this is the behaviour expected by the older version of Merge. This may change in a later version.
expectType<{
[x: number]: number;
[x: symbol]: boolean;
[x: string]: unknown;
b: string;
c: undefined;
a?: string;
d?: string;
e?: number;
f?: number;
g: undefined;
}>(fooBarWithOptionalKeys);

0 comments on commit 2e443e2

Please sign in to comment.