diff --git a/index.d.ts b/index.d.ts index 1c59799b6..520df2219 100644 --- a/index.d.ts +++ b/index.d.ts @@ -7,6 +7,7 @@ export {Mutable} from './source/mutable'; export {Merge} from './source/merge'; export {MergeExclusive} from './source/merge-exclusive'; export {RequireAtLeastOne} from './source/require-at-least-one'; +export {RequireExactlyOne} from './source/require-exactly-one'; export {PartialDeep} from './source/partial-deep'; export {ReadonlyDeep} from './source/readonly-deep'; export {LiteralUnion} from './source/literal-union'; diff --git a/readme.md b/readme.md index 3cf68afd5..590ee56d3 100644 --- a/readme.md +++ b/readme.md @@ -69,6 +69,7 @@ Click the type names for complete docs. - [`Merge`](source/merge.d.ts) - Merge two types into a new type. Keys of the second type overrides keys of the first type. - [`MergeExclusive`](source/merge-exclusive.d.ts) - Create a type that has mutually exclusive properties. - [`RequireAtLeastOne`](source/require-at-least-one.d.ts) - Create a type that requires at least one of the given properties. +- [`RequireExactlyOne`](source/require-one.d.ts) - Create a type that requires exactly a single property of the given properties and disallows more. - [`PartialDeep`](source/partial-deep.d.ts) - Create a deeply optional version of another type. Use [`Partial`](https://github.com/Microsoft/TypeScript/blob/2961bc3fc0ea1117d4e53bc8e97fa76119bc33e3/src/lib/es5.d.ts#L1401-L1406) if you only need one level deep. - [`ReadonlyDeep`](source/readonly-deep.d.ts) - Create a deeply immutable version of an `object`/`Map`/`Set`/`Array` type. Use [`Readonly`](https://github.com/Microsoft/TypeScript/blob/2961bc3fc0ea1117d4e53bc8e97fa76119bc33e3/src/lib/es5.d.ts#L1415-L1420) if you only need one level deep. - [`LiteralUnion`](source/literal-union.d.ts) - Create 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). diff --git a/source/require-exactly-one.d.ts b/source/require-exactly-one.d.ts new file mode 100644 index 000000000..2be16b71e --- /dev/null +++ b/source/require-exactly-one.d.ts @@ -0,0 +1,32 @@ +/** +Create a type that requires exactly one of the given properties and disallows more. The remaining properties are kept as is. + +Use-cases: +- Creating interfaces for components that only need one of the properties to display properly. +- Declaring generic properties in a single place for a single use-case that gets narrowed down via `RequireExactlyOne`. + +The caveat with `RequireExactlyOne` is that TypeScript doesn't always know at compile time every property that will exist at runtime. Therefore `RequireExactlyOne` can't do anything to prevent extra properties it doesn't know about. + +@example +``` +import {RequireExactlyOne} from 'type-fest'; + +type Responder = { + text: () => string; + json: () => string; + secure: boolean; +}; + +const responder: RequireExactlyOne = { + // Adding a `text` property here would cause a compile error. + + json: () => '{"message": "ok"}', + secure: true +}; +``` +*/ +export type RequireExactlyOne = + {[Key in KeysType]: ( + Required> & + Partial, never>> + )}[KeysType] & Omit; diff --git a/test-d/require-exactly-one.ts b/test-d/require-exactly-one.ts new file mode 100644 index 000000000..3b4564c56 --- /dev/null +++ b/test-d/require-exactly-one.ts @@ -0,0 +1,24 @@ +import {expectType, expectError} from 'tsd'; +import {RequireExactlyOne} from '..'; + +type SystemMessages = { + default: string; + + macos: string; + linux: string; + + optional?: string; +}; + +type ValidMessages = RequireExactlyOne; +const test = (_: ValidMessages): void => {}; + +test({macos: 'hey', default: 'hello'}); +test({linux: 'sup', optional: 'howdy', default: 'hello'}); + +expectError(test({})); +expectError(test({macos: 'hey', linux: 'sup', default: 'hello'})); + +declare const oneWithoutKeys: RequireExactlyOne<{a: number; b: number}>; +expectType<{a: number} | {b: number}>(oneWithoutKeys); +expectError(expectType<{a: number; b: number}>(oneWithoutKeys));