From 0a098c61bc9168f7cf8111f2b28a994a1bfbe0c3 Mon Sep 17 00:00:00 2001 From: Tommy Date: Sun, 30 Jul 2023 10:02:40 -0500 Subject: [PATCH] Add `RequireOneOrNone` type (#654) --- index.d.ts | 1 + readme.md | 1 + source/internal.d.ts | 5 +++ source/require-all-or-none.d.ts | 14 ++++++--- source/require-one-or-none.d.ts | 37 ++++++++++++++++++++++ test-d/internal.ts | 53 -------------------------------- test-d/internal/is-not-false.ts | 11 +++++++ test-d/internal/is-null.ts | 11 +++++++ test-d/internal/is-numeric.ts | 20 ++++++++++++ test-d/internal/is-whitespace.ts | 11 +++++++ test-d/internal/require-none.ts | 15 +++++++++ test-d/require-one-or-none.ts | 26 ++++++++++++++++ 12 files changed, 148 insertions(+), 57 deletions(-) create mode 100644 source/require-one-or-none.d.ts delete mode 100644 test-d/internal.ts create mode 100644 test-d/internal/is-not-false.ts create mode 100644 test-d/internal/is-null.ts create mode 100644 test-d/internal/is-numeric.ts create mode 100644 test-d/internal/is-whitespace.ts create mode 100644 test-d/internal/require-none.ts create mode 100644 test-d/require-one-or-none.ts diff --git a/index.d.ts b/index.d.ts index 0128a01e3..9a05a87df 100644 --- a/index.d.ts +++ b/index.d.ts @@ -16,6 +16,7 @@ export type {MergeExclusive} from './source/merge-exclusive'; export type {RequireAtLeastOne} from './source/require-at-least-one'; export type {RequireExactlyOne} from './source/require-exactly-one'; export type {RequireAllOrNone} from './source/require-all-or-none'; +export type {RequireOneOrNone} from './source/require-one-or-none'; export type {OmitIndexSignature} from './source/omit-index-signature'; export type {PickIndexSignature} from './source/pick-index-signature'; export type {PartialDeep, PartialDeepOptions} from './source/partial-deep'; diff --git a/readme.md b/readme.md index 7d9e75271..1296c3431 100644 --- a/readme.md +++ b/readme.md @@ -120,6 +120,7 @@ Click the type names for complete docs. - [`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. +- [`RequireOneOrNone`](source/require-one-or-none.d.ts) - Create a type that requires exactly a single key of the given keys and disallows more, or none of the given keys. - [`RequiredDeep`](source/required-deep.d.ts) - Create a deeply required version of another type. Use [`Required`](https://www.typescriptlang.org/docs/handbook/utility-types.html#requiredtype) if you only need one level deep. - [`OmitIndexSignature`](source/omit-index-signature.d.ts) - Omit any index signatures from the given object type, leaving only explicitly defined properties. - [`PickIndexSignature`](source/pick-index-signature.d.ts) - Pick only index signatures from the given object type, leaving out all explicitly defined properties. diff --git a/source/internal.d.ts b/source/internal.d.ts index 8642f60d3..bb3691157 100644 --- a/source/internal.d.ts +++ b/source/internal.d.ts @@ -251,3 +251,8 @@ export type IsNotFalse = [T] extends [false] ? false : true; Returns a boolean for whether the given type is `null`. */ export type IsNull = [T] extends [null] ? true : false; + +/** +Disallows any of the given keys. +*/ +export type RequireNone = Partial>; diff --git a/source/require-all-or-none.d.ts b/source/require-all-or-none.d.ts index 4b50c2a6c..11787970e 100644 --- a/source/require-all-or-none.d.ts +++ b/source/require-all-or-none.d.ts @@ -1,3 +1,10 @@ +import type {RequireNone} from './internal'; + +/** +Requires all of the keys in the given object. +*/ +type RequireAll = Required>; + /** Create a type that requires all of the given keys or none of the given keys. The remaining keys are kept as is. @@ -30,7 +37,6 @@ const responder2: RequireAllOrNone = { @category Object */ export type RequireAllOrNone = ( - | Required> // Require all of the given keys. - | Partial> // Require none of the given keys. -) & -Omit; // The rest of the keys. + | RequireAll + | RequireNone +) & Omit; // The rest of the keys. diff --git a/source/require-one-or-none.d.ts b/source/require-one-or-none.d.ts new file mode 100644 index 000000000..5227382ec --- /dev/null +++ b/source/require-one-or-none.d.ts @@ -0,0 +1,37 @@ +import type {RequireExactlyOne} from './require-exactly-one'; +import type {RequireNone} from './internal'; + +/** +Create a type that requires exactly one of the given keys and disallows more, or none of the given keys. The remaining keys are kept as is. + +@example +``` +import type {RequireOneOrNone} from 'type-fest'; + +type Responder = RequireOneOrNone<{ + text: () => string; + json: () => string; + secure: boolean; +}, 'text' | 'json'>; + +const responder1: Responder = { + secure: true +}; + +const responder2: Responder = { + text: () => '{"message": "hi"}', + secure: true +}; + +const responder3: Responder = { + json: () => '{"message": "ok"}', + secure: true +}; +``` + +@category Object +*/ +export type RequireOneOrNone = ( + | RequireExactlyOne + | RequireNone +) & Omit; // Ignore unspecified keys. diff --git a/test-d/internal.ts b/test-d/internal.ts deleted file mode 100644 index 5f3b7325f..000000000 --- a/test-d/internal.ts +++ /dev/null @@ -1,53 +0,0 @@ -import {expectType} from 'tsd'; -import type { - IsWhitespace, - IsNumeric, - IsNotFalse, - IsNull, -} from '../source/internal'; - -expectType>(false); -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(false); -expectType>(false); -expectType>(true); -expectType>(true); - -expectType>(false); -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(false); -expectType>(false); -expectType>(false); -expectType>(false); -expectType>(false); -expectType>(false); -expectType>(false); - -/* eslint-disable @typescript-eslint/no-duplicate-type-constituents */ -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(false); -expectType>(false); -expectType>(false); -/* eslint-enable @typescript-eslint/no-duplicate-type-constituents */ - -// https://www.typescriptlang.org/docs/handbook/type-compatibility.html -expectType>(true); -expectType>(true); -expectType>(true); -expectType>(false); // Depends on `strictNullChecks` -expectType>(false); -expectType>(false); -expectType>(false); diff --git a/test-d/internal/is-not-false.ts b/test-d/internal/is-not-false.ts new file mode 100644 index 000000000..a7fac16a9 --- /dev/null +++ b/test-d/internal/is-not-false.ts @@ -0,0 +1,11 @@ +/* eslint-disable @typescript-eslint/no-duplicate-type-constituents */ +import {expectType} from 'tsd'; +import type {IsNotFalse} from '../../source/internal'; + +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(false); +expectType>(false); +expectType>(false); diff --git a/test-d/internal/is-null.ts b/test-d/internal/is-null.ts new file mode 100644 index 000000000..2cc6d66e1 --- /dev/null +++ b/test-d/internal/is-null.ts @@ -0,0 +1,11 @@ +import {expectType} from 'tsd'; +import type {IsNull} from '../../source/internal'; + +// https://www.typescriptlang.org/docs/handbook/type-compatibility.html +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(false); // Depends on `strictNullChecks` +expectType>(false); +expectType>(false); +expectType>(false); diff --git a/test-d/internal/is-numeric.ts b/test-d/internal/is-numeric.ts new file mode 100644 index 000000000..4b14e557d --- /dev/null +++ b/test-d/internal/is-numeric.ts @@ -0,0 +1,20 @@ +import {expectType} from 'tsd'; +import type {IsNumeric} from '../../source/internal'; + +expectType>(false); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); diff --git a/test-d/internal/is-whitespace.ts b/test-d/internal/is-whitespace.ts new file mode 100644 index 000000000..2e087a9bf --- /dev/null +++ b/test-d/internal/is-whitespace.ts @@ -0,0 +1,11 @@ +import {expectType} from 'tsd'; +import type {IsWhitespace} from '../../source/internal'; + +expectType>(false); +expectType>(true); +expectType>(true); +expectType>(true); +expectType>(false); +expectType>(false); +expectType>(true); +expectType>(true); diff --git a/test-d/internal/require-none.ts b/test-d/internal/require-none.ts new file mode 100644 index 000000000..75ccdea30 --- /dev/null +++ b/test-d/internal/require-none.ts @@ -0,0 +1,15 @@ +import {expectAssignable, expectNotAssignable, expectType} from 'tsd'; +import {type RequireNone} from '../../source/internal'; + +type NoneAllowed = RequireNone<'foo' | 'bar'>; + +expectAssignable({}); +expectNotAssignable({foo: 'foo'}); +expectNotAssignable({bar: 'bar'}); +expectNotAssignable({foo: 'foo', bar: 'bar'}); + +type SomeAllowed = Record<'bar', string> & RequireNone<'foo'>; + +expectAssignable({bar: 'bar'}); +expectNotAssignable({foo: 'foo'}); +expectNotAssignable({foo: 'foo', bar: 'bar'}); diff --git a/test-d/require-one-or-none.ts b/test-d/require-one-or-none.ts new file mode 100644 index 000000000..8b07dd031 --- /dev/null +++ b/test-d/require-one-or-none.ts @@ -0,0 +1,26 @@ +import {expectAssignable, expectNotAssignable} from 'tsd'; +import type {RequireOneOrNone} from '../index'; + +type OneAtMost = RequireOneOrNone>; + +expectAssignable({}); +expectAssignable({foo: true}); +expectAssignable({bar: true}); +expectAssignable({baz: true}); + +expectNotAssignable({foo: true, bar: true}); +expectNotAssignable({foo: true, baz: true}); +expectNotAssignable({bar: true, baz: true}); +expectNotAssignable({foo: true, bar: true, baz: true}); + +// 'foo' always required +type OneOrTwo = RequireOneOrNone, 'bar' | 'baz'>; + +expectAssignable({foo: true}); +expectAssignable({foo: true, bar: true}); +expectAssignable({foo: true, baz: true}); + +expectNotAssignable({}); +expectNotAssignable({bar: true}); +expectNotAssignable({baz: true}); +expectNotAssignable({foo: true, bar: true, baz: true});