diff --git a/source/enforce-optional.d.ts b/source/enforce-optional.d.ts new file mode 100644 index 000000000..909f5939d --- /dev/null +++ b/source/enforce-optional.d.ts @@ -0,0 +1,47 @@ +import type {Simplify} from './simplify'; + +// Returns `never` if the key is optional otherwise return the key type. +type RequiredFilter = 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 = 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; +// => { +// a: string; +// b?: string; +// c: undefined; +// d?: number; +// } +``` + +@internal +@category Object +*/ +export type EnforceOptional = Simplify<{ + [Key in keyof ObjectType as RequiredFilter]: ObjectType[Key] +} & { + [Key in keyof ObjectType as OptionalFilter]?: Exclude +}>; diff --git a/source/merge.d.ts b/source/merge.d.ts index 76f26d010..7984ecd31 100644 --- a/source/merge.d.ts +++ b/source/merge.d.ts @@ -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. @@ -36,10 +36,10 @@ export type FooBar = Merge; @category Object */ -export type Merge = Simplify<{ - [Key in keyof OmitIndexSignature]: Key extends keyof Source +export type Merge = EnforceOptional<{ + [Key in keyof OmitIndexSignature | keyof OmitIndexSignature]: Key extends keyof Source ? Source[Key] : Key extends keyof Destination ? Destination[Key] : never; -} & PickIndexSignature>; +} & PickIndexSignature & PickIndexSignature>; diff --git a/test-d/enforce-optional.ts b/test-d/enforce-optional.ts new file mode 100644 index 000000000..1b008294d --- /dev/null +++ b/test-d/enforce-optional.ts @@ -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; + +declare const enforcedOptionalFoo: EnforcedOptionalFoo; + +expectType<{ + a: string; + b?: string; + c: undefined; + d?: number; +}>(enforcedOptionalFoo); diff --git a/test-d/merge.ts b/test-d/merge.ts index ae5802456..069e0df01 100644 --- a/test-d/merge.ts +++ b/test-d/merge.ts @@ -1,4 +1,4 @@ -import {expectAssignable, expectError} from 'tsd'; +import {expectError, expectType} from 'tsd'; import type {Merge} from '../index'; type Foo = { @@ -11,7 +11,7 @@ type Bar = { }; const ab: Merge = {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 { @@ -39,7 +39,7 @@ const fooBar: FooBar = { baz: true, }; -expectAssignable<{ +expectType<{ [x: string]: unknown; [x: number]: number; [x: symbol]: boolean; @@ -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; + +expectType({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; + +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);