Skip to content

Commit

Permalink
Add RequireOneOrNone type (#654)
Browse files Browse the repository at this point in the history
  • Loading branch information
tommy-mitchell authored Jul 30, 2023
1 parent 51eb3f7 commit 0a098c6
Show file tree
Hide file tree
Showing 12 changed files with 148 additions and 57 deletions.
1 change: 1 addition & 0 deletions index.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand Down
1 change: 1 addition & 0 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -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<T>`](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.
Expand Down
5 changes: 5 additions & 0 deletions source/internal.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -251,3 +251,8 @@ export type IsNotFalse<T extends boolean> = [T] extends [false] ? false : true;
Returns a boolean for whether the given type is `null`.
*/
export type IsNull<T> = [T] extends [null] ? true : false;

/**
Disallows any of the given keys.
*/
export type RequireNone<KeysType extends PropertyKey> = Partial<Record<KeysType, never>>;
14 changes: 10 additions & 4 deletions source/require-all-or-none.d.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
import type {RequireNone} from './internal';

/**
Requires all of the keys in the given object.
*/
type RequireAll<ObjectType, KeysType extends keyof ObjectType> = Required<Pick<ObjectType, KeysType>>;

/**
Create a type that requires all of the given keys or none of the given keys. The remaining keys are kept as is.
Expand Down Expand Up @@ -30,7 +37,6 @@ const responder2: RequireAllOrNone<Responder, 'text' | 'json'> = {
@category Object
*/
export type RequireAllOrNone<ObjectType, KeysType extends keyof ObjectType = never> = (
| Required<Pick<ObjectType, KeysType>> // Require all of the given keys.
| Partial<Record<KeysType, never>> // Require none of the given keys.
) &
Omit<ObjectType, KeysType>; // The rest of the keys.
| RequireAll<ObjectType, KeysType>
| RequireNone<KeysType>
) & Omit<ObjectType, KeysType>; // The rest of the keys.
37 changes: 37 additions & 0 deletions source/require-one-or-none.d.ts
Original file line number Diff line number Diff line change
@@ -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<ObjectType, KeysType extends keyof ObjectType = keyof ObjectType> = (
| RequireExactlyOne<ObjectType, KeysType>
| RequireNone<KeysType>
) & Omit<ObjectType, KeysType>; // Ignore unspecified keys.
53 changes: 0 additions & 53 deletions test-d/internal.ts

This file was deleted.

11 changes: 11 additions & 0 deletions test-d/internal/is-not-false.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/* eslint-disable @typescript-eslint/no-duplicate-type-constituents */
import {expectType} from 'tsd';
import type {IsNotFalse} from '../../source/internal';

expectType<IsNotFalse<true>>(true);
expectType<IsNotFalse<boolean>>(true);
expectType<IsNotFalse<true | false>>(true);
expectType<IsNotFalse<true | false | false | false>>(true);
expectType<IsNotFalse<false>>(false);
expectType<IsNotFalse<false | false>>(false);
expectType<IsNotFalse<false | false | false | false>>(false);
11 changes: 11 additions & 0 deletions test-d/internal/is-null.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {expectType} from 'tsd';
import type {IsNull} from '../../source/internal';

// https://www.typescriptlang.org/docs/handbook/type-compatibility.html
expectType<IsNull<null>>(true);
expectType<IsNull<any>>(true);
expectType<IsNull<never>>(true);
expectType<IsNull<undefined>>(false); // Depends on `strictNullChecks`
expectType<IsNull<unknown>>(false);
expectType<IsNull<void>>(false);
expectType<IsNull<{}>>(false);
20 changes: 20 additions & 0 deletions test-d/internal/is-numeric.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import {expectType} from 'tsd';
import type {IsNumeric} from '../../source/internal';

expectType<IsNumeric<''>>(false);
expectType<IsNumeric<'0'>>(true);
expectType<IsNumeric<'1'>>(true);
expectType<IsNumeric<'-1'>>(true);
expectType<IsNumeric<'123'>>(true);
expectType<IsNumeric<'1e2'>>(true);
expectType<IsNumeric<'1.23'>>(true);
expectType<IsNumeric<'123.456'>>(true);
expectType<IsNumeric<'1.23e4'>>(true);
expectType<IsNumeric<'1.23e-4'>>(true);
expectType<IsNumeric<' '>>(false);
expectType<IsNumeric<'\n'>>(false);
expectType<IsNumeric<'\u{9}'>>(false);
expectType<IsNumeric<' 1.2'>>(false);
expectType<IsNumeric<'1 2'>>(false);
expectType<IsNumeric<'1_200'>>(false);
expectType<IsNumeric<' 1 '>>(false);
11 changes: 11 additions & 0 deletions test-d/internal/is-whitespace.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import {expectType} from 'tsd';
import type {IsWhitespace} from '../../source/internal';

expectType<IsWhitespace<''>>(false);
expectType<IsWhitespace<' '>>(true);
expectType<IsWhitespace<'\n'>>(true);
expectType<IsWhitespace<'\u{9}'>>(true);
expectType<IsWhitespace<'a'>>(false);
expectType<IsWhitespace<'a '>>(false);
expectType<IsWhitespace<' '>>(true);
expectType<IsWhitespace<' \t '>>(true);
15 changes: 15 additions & 0 deletions test-d/internal/require-none.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import {expectAssignable, expectNotAssignable, expectType} from 'tsd';
import {type RequireNone} from '../../source/internal';

type NoneAllowed = RequireNone<'foo' | 'bar'>;

expectAssignable<NoneAllowed>({});
expectNotAssignable<NoneAllowed>({foo: 'foo'});
expectNotAssignable<NoneAllowed>({bar: 'bar'});
expectNotAssignable<NoneAllowed>({foo: 'foo', bar: 'bar'});

type SomeAllowed = Record<'bar', string> & RequireNone<'foo'>;

expectAssignable<SomeAllowed>({bar: 'bar'});
expectNotAssignable<SomeAllowed>({foo: 'foo'});
expectNotAssignable<SomeAllowed>({foo: 'foo', bar: 'bar'});
26 changes: 26 additions & 0 deletions test-d/require-one-or-none.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import {expectAssignable, expectNotAssignable} from 'tsd';
import type {RequireOneOrNone} from '../index';

type OneAtMost = RequireOneOrNone<Record<'foo' | 'bar' | 'baz', true>>;

expectAssignable<OneAtMost>({});
expectAssignable<OneAtMost>({foo: true});
expectAssignable<OneAtMost>({bar: true});
expectAssignable<OneAtMost>({baz: true});

expectNotAssignable<OneAtMost>({foo: true, bar: true});
expectNotAssignable<OneAtMost>({foo: true, baz: true});
expectNotAssignable<OneAtMost>({bar: true, baz: true});
expectNotAssignable<OneAtMost>({foo: true, bar: true, baz: true});

// 'foo' always required
type OneOrTwo = RequireOneOrNone<Record<'foo' | 'bar' | 'baz', true>, 'bar' | 'baz'>;

expectAssignable<OneOrTwo>({foo: true});
expectAssignable<OneOrTwo>({foo: true, bar: true});
expectAssignable<OneOrTwo>({foo: true, baz: true});

expectNotAssignable<OneOrTwo>({});
expectNotAssignable<OneOrTwo>({bar: true});
expectNotAssignable<OneOrTwo>({baz: true});
expectNotAssignable<OneOrTwo>({foo: true, bar: true, baz: true});

0 comments on commit 0a098c6

Please sign in to comment.