diff --git a/index.d.ts b/index.d.ts index 013785fa8..e0126fee3 100644 --- a/index.d.ts +++ b/index.d.ts @@ -198,3 +198,36 @@ export type RequireAtLeastOne; + +/* +Create a type that requires at only one of the given properties. The remaining properties are kept as is. + +@example +``` +import {RequireOnlyOne} from 'type-fest'; + +type Responder = { + text: () => string; + json: () => string; + + secure: boolean; +}; + +const responder: RequireOnlyOne = { + json: () => '{"message": "ok"}', + secure: true +}; +``` +*/ +export type RequireOnlyOne = + { + // For each Key in KeysType make a mapped type + [Key in KeysType]: ( + // …by picking that Key's type and making it required + Required> & + // …and adding the other keys in KeysType as optional and of type `never` + Partial, never>> + ) + }[KeysType] + // …then, make intersection types by adding the remaining properties to each mapped type. + & Omit; diff --git a/readme.md b/readme.md index d38e26035..e6408dbaa 100644 --- a/readme.md +++ b/readme.md @@ -66,6 +66,7 @@ See the [types file](index.d.ts) for complete docs. - `Merge` - Merge two types into a new type. Keys of the second type overrides keys of the first type. - `MergeExclusive` - Create a type that has mutually exclusive properties. - `RequireAtLeastOne` - Create a type that requires at least one of the given properties. +- `RequireOnlyOne` - Create a type that requires only one of the given properties. - `LiteralUnion` - Allows creating a union type by combining primitive types and literal types without sacrificing auto-completion in IDEs for the literal type part of the union. Workaround for [Microsoft/TypeScript#29729](https://github.com/Microsoft/TypeScript/issues/29729). ### Miscellaneous diff --git a/test-d/require-only-one.ts b/test-d/require-only-one.ts new file mode 100644 index 000000000..138e5cffb --- /dev/null +++ b/test-d/require-only-one.ts @@ -0,0 +1,38 @@ +import {expectType, expectError} from 'tsd'; +import {RequireOnlyOne} from '..'; + +type SystemMessages = { + default: string; + + macos?: string; + linux: string; + windows: string; + + optional?: string; +}; + +type ValidMessages = RequireOnlyOne; +const test = (_: ValidMessages): void => {}; + +test({macos: 'hey', default: 'hello'}); +test({linux: 'sup', default: 'hello'}); +test({windows: 'hi', default: 'hello'}); +test({windows: 'hi', default: 'hello', optional: 'howdy'}); + +const system = Math.random() > 0.5 ? {macos: 'hey'} : {linux: 'sup'}; +test({default: 'hello', ...system}); + +expectError(test({})); +expectError(test({macos: 'hey'})); +expectError(test({default: 'hello'})); +expectError(test({macos: 'hey', linux: 'sup', default: 'hello'})); +expectError(test({macos: 'hey', default: 'hello', unknown: 'hmmm'})); + +// TODO: Should it eventually be possible to make this negative test pass? +/* +const invalidSystem = Math.random() > 0.5 ? {default: 'hello', macos: 'hey', a: 1, b: 3} : {default: 'hello', linux: 'sup', b: 2}; +expectError(test(invalidSystem)); +*/ + +declare const onlyOneWithoutKeys: RequireOnlyOne<{a: number; b: number}>; +expectType<{a: number} | {b: number}>(onlyOneWithoutKeys);