From 28199b08206a955c78098e650d0f9f73331dd708 Mon Sep 17 00:00:00 2001 From: Aaron Harper Date: Thu, 9 Sep 2021 23:39:53 -0400 Subject: [PATCH] Add `RequireAllOrNone` type (#254) --- index.d.ts | 1 + readme.md | 1 + source/require-all-or-none.ts | 36 +++++++++++++++++++++++++++++++++++ test-d/require-all-or-none.ts | 24 +++++++++++++++++++++++ 4 files changed, 62 insertions(+) create mode 100644 source/require-all-or-none.ts create mode 100644 test-d/require-all-or-none.ts diff --git a/index.d.ts b/index.d.ts index 4c81fb06e..d362fbe9c 100644 --- a/index.d.ts +++ b/index.d.ts @@ -11,6 +11,7 @@ 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 {RequireAllOrNone} from './source/require-all-or-none'; 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 ab54fc76d..5ea80587b 100644 --- a/readme.md +++ b/readme.md @@ -100,6 +100,7 @@ Click the type names for complete docs. - [`MergeExclusive`](source/merge-exclusive.d.ts) - Create a type that has mutually exclusive keys. - [`RequireAtLeastOne`](source/require-at-least-one.d.ts) - Create a type that requires at least one of the given keys. - [`RequireExactlyOne`](source/require-exactly-one.d.ts) - Create a type that requires exactly a single key of the given keys and disallows more. +- [`RequireAllOrNone`](source/require-all-or-none.d.ts) - Create a type that requires all of the given keys or none of the given keys. - [`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-all-or-none.ts b/source/require-all-or-none.ts new file mode 100644 index 000000000..16faca724 --- /dev/null +++ b/source/require-all-or-none.ts @@ -0,0 +1,36 @@ +/** +Create a type that requires all of the given keys or none of the given keys. The remaining keys are kept as is. + +Use-cases: +- Creating interfaces for components with mutually-inclusive keys. + +The caveat with `RequireAllOrNone` is that TypeScript doesn't always know at compile time every key that will exist at runtime. Therefore `RequireAllOrNone` can't do anything to prevent extra keys it doesn't know about. + +@example +``` +import {RequireAllOrNone} from 'type-fest'; + +type Responder = { + text?: () => string; + json?: () => string; + secure: boolean; +}; + +const responder1: RequireAllOrNone = { + secure: true +}; + +const responder2: RequireAllOrNone = { + text: () => '{"message": "hi"}', + json: () => '{"message": "ok"}', + secure: true +}; +``` + +@category Utilities +*/ +export type RequireAllOrNone = ( + | Required> // Require all of the given keys. + | Partial> // Require none of the given keys. +) & + Omit; // The rest of the keys. diff --git a/test-d/require-all-or-none.ts b/test-d/require-all-or-none.ts new file mode 100644 index 000000000..ef97a19c6 --- /dev/null +++ b/test-d/require-all-or-none.ts @@ -0,0 +1,24 @@ +import {expectError, expectAssignable} from 'tsd'; +import {RequireAllOrNone} from '../index'; + +type SystemMessages = { + default: string; + + macos: string; + linux: string; + + optional?: string; +}; + +type ValidMessages = RequireAllOrNone; +const test = (_: ValidMessages): void => {}; // eslint-disable-line @typescript-eslint/no-empty-function + +test({default: 'hello'}); +test({macos: 'yo', linux: 'sup', optional: 'howdy', default: 'hello'}); + +expectError(test({})); +expectError(test({macos: 'hey', default: 'hello'})); +expectError(test({linux: 'hey', default: 'hello'})); + +declare const oneWithoutKeys: RequireAllOrNone<{a: number; b: number}>; +expectAssignable<{a: number; b: number}>(oneWithoutKeys);