From 40457374997e076142206f9d9f2123d926490c4f Mon Sep 17 00:00:00 2001 From: Tommy Date: Wed, 5 Apr 2023 10:07:21 -0500 Subject: [PATCH] Add `IsX`/`IfX` types for `any`/`never`/`unknown` (#564) Co-authored-by: Sindre Sorhus --- index.d.ts | 6 +++++ readme.md | 33 ++++++++++++++++++++++- source/if-any.d.ts | 24 +++++++++++++++++ source/if-never.d.ts | 24 +++++++++++++++++ source/if-unknown.d.ts | 24 +++++++++++++++++ source/internal.d.ts | 23 +++++----------- source/is-any.d.ts | 29 +++++++++++++++++++++ source/is-equal.d.ts | 1 + source/is-literal.d.ts | 13 +++++----- source/is-never.d.ts | 49 ++++++++++++++++++++++++++++++++++ source/is-unknown.d.ts | 52 +++++++++++++++++++++++++++++++++++++ source/jsonify.d.ts | 3 ++- source/set-return-type.d.ts | 2 +- test-d/if-any.ts | 13 ++++++++++ test-d/if-never.ts | 13 ++++++++++ test-d/if-unknown.ts | 13 ++++++++++ test-d/internal.ts | 16 +++++++++++- test-d/is-any.ts | 19 ++++++++++++++ test-d/is-never.ts | 19 ++++++++++++++ test-d/is-unknown.ts | 19 ++++++++++++++ 20 files changed, 368 insertions(+), 27 deletions(-) create mode 100644 source/if-any.d.ts create mode 100644 source/if-never.d.ts create mode 100644 source/if-unknown.d.ts create mode 100644 source/is-any.d.ts create mode 100644 source/is-never.d.ts create mode 100644 source/is-unknown.d.ts create mode 100644 test-d/if-any.ts create mode 100644 test-d/if-never.ts create mode 100644 test-d/if-unknown.ts create mode 100644 test-d/is-any.ts create mode 100644 test-d/is-never.ts create mode 100644 test-d/is-unknown.ts diff --git a/index.d.ts b/index.d.ts index 83204bda4..1abfd3588 100644 --- a/index.d.ts +++ b/index.d.ts @@ -85,6 +85,12 @@ export type { IsBooleanLiteral, IsSymbolLiteral, } from './source/is-literal'; +export type {IsAny} from './source/is-any'; +export type {IfAny} from './source/if-any'; +export type {IsNever} from './source/is-never'; +export type {IfNever} from './source/if-never'; +export type {IsUnknown} from './source/is-unknown'; +export type {IfUnknown} from './source/if-unknown'; // Template literal types export type {CamelCase} from './source/camel-case'; diff --git a/readme.md b/readme.md index 568548fcf..fb4f8f77e 100644 --- a/readme.md +++ b/readme.md @@ -174,12 +174,43 @@ Click the type names for complete docs. - [`HasRequiredKeys`](source/has-required-keys.d.ts) - Create a `true`/`false` type depending on whether the given type has any required fields. - [`Spread`](source/spread.d.ts) - Mimic the type inferred by TypeScript when merging two objects or two arrays/tuples using the spread syntax. - [`IsEqual`](source/is-equal.d.ts) - Returns a boolean for whether the two given types are equal. +- [`TaggedUnion`](source/tagged-union.d.ts) - Create a union of types that share a common discriminant property. + +### Type Guard + +#### `IsType` vs. `IfType` + +For every `IsT` type (e.g. `IsAny`), there is an associated `IfT` type that can help simplify conditional types. While the `IsT` types return a `boolean`, the `IfT` types act like an `If`/`Else` - they resolve to the given `TypeIfT` or `TypeIfNotT` depending on whether `IsX` is `true` or not. By default, `IfT` returns a `boolean`: + +```ts +type IfAny = ( + IsAny extends true ? TypeIfAny : TypeIfNotAny +); +``` + +#### Usage + +```ts +import type {IsAny, IfAny} from 'type-fest'; + +type ShouldBeTrue = IsAny extends true ? true : false; +//=> true + +type ShouldBeFalse = IfAny<'not any'>; +//=> false + +type ShouldBeNever = IfAny<'not any', 'not never', 'never'>; +//=> 'never' +``` + - [`IsLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). - [`IsStringLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `string` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). - [`IsNumericLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `number` or `bigint` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). - [`IsBooleanLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `true` or `false` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). - [`IsSymbolLiteral`](source/is-literal.d.ts) - Returns a boolean for whether the given type is a `symbol` [literal type](https://www.typescriptlang.org/docs/handbook/2/everyday-types.html#literal-types). -- [`TaggedUnion`](source/tagged-union.d.ts) - Create a union of types that share a common discriminant property. +- [`IsAny`](source/is-any.d.ts) - Returns a boolean for whether the given type is `any`. (Conditional version: [`IfAny`](source/if-any.d.ts).) +- [`IsNever`](source/is-never.d.ts) - Returns a boolean for whether the given type is `never`. (Conditional version: [`IfNever`](source/if-never.d.ts).) +- [`IsUnknown`](source/is-unknown.d.ts) - Returns a boolean for whether the given type is `unknown`. (Conditional version: [`IfUnknown`](source/if-unknown.d.ts).) ### JSON diff --git a/source/if-any.d.ts b/source/if-any.d.ts new file mode 100644 index 000000000..1c17b76f6 --- /dev/null +++ b/source/if-any.d.ts @@ -0,0 +1,24 @@ +import type {IsAny} from './is-any'; + +/** +An if-else-like type that resolves depending on whether the given type is `any`. + +@see {@link IsAny} + +@example +``` +import type {IfAny} from 'type-fest'; + +type ShouldBeTrue = IfAny; +//=> true + +type ShouldBeBar = IfAny<'not any', 'foo', 'bar'>; +//=> 'bar' +``` + +@category Type Guard +@category Utilities +*/ +export type IfAny = ( + IsAny extends true ? TypeIfAny : TypeIfNotAny +); diff --git a/source/if-never.d.ts b/source/if-never.d.ts new file mode 100644 index 000000000..d33af2e8c --- /dev/null +++ b/source/if-never.d.ts @@ -0,0 +1,24 @@ +import type {IsNever} from './is-never'; + +/** +An if-else-like type that resolves depending on whether the given type is `never`. + +@see {@link IsNever} + +@example +``` +import type {IfNever} from 'type-fest'; + +type ShouldBeTrue = IfNever; +//=> true + +type ShouldBeBar = IfNever<'not never', 'foo', 'bar'>; +//=> 'bar' +``` + +@category Type Guard +@category Utilities +*/ +export type IfNever = ( + IsNever extends true ? TypeIfNever : TypeIfNotNever +); diff --git a/source/if-unknown.d.ts b/source/if-unknown.d.ts new file mode 100644 index 000000000..828268c56 --- /dev/null +++ b/source/if-unknown.d.ts @@ -0,0 +1,24 @@ +import type {IsUnknown} from './is-unknown'; + +/** +An if-else-like type that resolves depending on whether the given type is `unknown`. + +@see {@link IsUnknown} + +@example +``` +import type {IfUnknown} from 'type-fest'; + +type ShouldBeTrue = IfUnknown; +//=> true + +type ShouldBeBar = IfUnknown<'not unknown', 'foo', 'bar'>; +//=> 'bar' +``` + +@category Type Guard +@category Utilities +*/ +export type IfUnknown = ( + IsUnknown extends true ? TypeIfUnknown : TypeIfNotUnknown +); diff --git a/source/internal.d.ts b/source/internal.d.ts index 6870d2b1b..4daea93bd 100644 --- a/source/internal.d.ts +++ b/source/internal.d.ts @@ -1,6 +1,7 @@ import type {Primitive} from './primitive'; import type {Simplify} from './simplify'; import type {Trim} from './trim'; +import type {IsAny} from './is-any'; /** Infer the length of the given array ``. @@ -158,23 +159,6 @@ export type IsNumeric = T extends `${number}` : false : false; -/** -Returns a boolean for whether the the type is `any`. - -@link https://stackoverflow.com/a/49928360/1490091 -*/ -export type IsAny = 0 extends 1 & T ? true : false; - -/** -Returns a boolean for whether the the type is `never`. -*/ -export type IsNever = [T] extends [never] ? true : false; - -/** -Returns a boolean for whether the the type is `unknown`. -*/ -export type IsUnknown = IsNever extends false ? T extends unknown ? unknown extends T ? IsAny extends false ? true : false : false : false : false; - /** For an object T, if it has any properties that are a union with `undefined`, make those into optional properties instead. @@ -262,3 +246,8 @@ export type HasMultipleCallSignatures unknown Returns a boolean for whether the given `boolean` is not `false`. */ 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; diff --git a/source/is-any.d.ts b/source/is-any.d.ts new file mode 100644 index 000000000..9f1c3ec35 --- /dev/null +++ b/source/is-any.d.ts @@ -0,0 +1,29 @@ +/** +Returns a boolean for whether the given type is `any`. + +@link https://stackoverflow.com/a/49928360/1490091 + +Useful in type utilities, such as disallowing `any`s to be passed to a function. + +@example +``` +import type {IsAny} from 'type-fest'; + +const typedObject = {a: 1, b: 2} as const; +const anyObject: any = {a: 1, b: 2}; + +function get extends true ? {} : Record), K extends keyof O = keyof O>(obj: O, key: K) { + return obj[key]; +} + +const typedA = get(typedObject, 'a'); +//=> 1 + +const anyA = get(anyObject, 'a'); +//=> any +``` + +@category Type Guard +@category Utilities +*/ +export type IsAny = 0 extends 1 & T ? true : false; diff --git a/source/is-equal.d.ts b/source/is-equal.d.ts index 4a64dda57..5a8a39ba4 100644 --- a/source/is-equal.d.ts +++ b/source/is-equal.d.ts @@ -21,6 +21,7 @@ type Includes = : false; ``` +@category Type Guard @category Utilities */ export type IsEqual = diff --git a/source/is-literal.d.ts b/source/is-literal.d.ts index ef458cab1..a9e4c68b7 100644 --- a/source/is-literal.d.ts +++ b/source/is-literal.d.ts @@ -1,6 +1,7 @@ import type {Primitive} from './primitive'; import type {Numeric} from './numeric'; -import type {IsNever, IsNotFalse} from './internal'; +import type {IsNotFalse} from './internal'; +import type {IsNever} from './is-never'; /** Returns a boolean for whether the given type `T` is the specified `LiteralType`. @@ -77,8 +78,8 @@ const output = capitalize('hello, world!'); //=> 'Hello, world!' ``` -@category Utilities @category Type Guard +@category Utilities */ export type IsStringLiteral = LiteralCheck; @@ -125,8 +126,8 @@ endsWith('abc123', end); //=> boolean ``` -@category Utilities @category Type Guard +@category Utilities */ export type IsNumericLiteral = LiteralChecks; @@ -165,8 +166,8 @@ const eitherId = getId({asString: runtimeBoolean}); //=> number | string ``` -@category Utilities @category Type Guard +@category Utilities */ export type IsBooleanLiteral = LiteralCheck; @@ -200,8 +201,8 @@ get({[symbolValue]: 1} as const, symbolValue); //=> number ``` -@category Utilities @category Type Guard +@category Utilities */ export type IsSymbolLiteral = LiteralCheck; @@ -246,7 +247,7 @@ stripLeading(str, 'abc'); //=> string ``` -@category Utilities @category Type Guard +@category Utilities */ export type IsLiteral = IsNotFalse>; diff --git a/source/is-never.d.ts b/source/is-never.d.ts new file mode 100644 index 000000000..740ddc434 --- /dev/null +++ b/source/is-never.d.ts @@ -0,0 +1,49 @@ +/** +Returns a boolean for whether the given type is `never`. + +@link https://github.com/microsoft/TypeScript/issues/31751#issuecomment-498526919 +@link https://stackoverflow.com/a/53984913/10292952 +@link https://www.zhenghao.io/posts/ts-never + +Useful in type utilities, such as checking if something does not occur. + +@example +``` +import type {IsNever} from 'type-fest'; + +type And = + A extends true + ? B extends true + ? true + : false + : false; + +// https://github.com/andnp/SimplyTyped/blob/master/src/types/strings.ts +type AreStringsEqual = + And< + IsNever> extends true ? true : false, + IsNever> extends true ? true : false + >; + +type EndIfEqual = + AreStringsEqual extends true + ? never + : void; + +function endIfEqual(input: I, output: O): EndIfEqual { + if (input === output) { + process.exit(0); + } +} + +endIfEqual('abc', 'abc'); +//=> never + +endIfEqual('abc', '123'); +//=> void +``` + +@category Type Guard +@category Utilities +*/ +export type IsNever = [T] extends [never] ? true : false; diff --git a/source/is-unknown.d.ts b/source/is-unknown.d.ts new file mode 100644 index 000000000..eef4afd4e --- /dev/null +++ b/source/is-unknown.d.ts @@ -0,0 +1,52 @@ +import type {IsNull} from './internal'; + +/** +Returns a boolean for whether the given type is `unknown`. + +@link https://github.com/dsherret/conditional-type-checks/pull/16 + +Useful in type utilities, such as when dealing with unknown data from API calls. + +@example +``` +import type {IsUnknown} from 'type-fest'; + +// https://github.com/pajecawav/tiny-global-store/blob/master/src/index.ts +type Action = + IsUnknown extends true + ? (state: TState) => TState, + : (state: TState, payload: TPayload) => TState; + +class Store { + constructor(private state: TState) {} + + execute(action: Action, payload?: TPayload): TState { + this.state = action(this.state, payload); + return this.state; + } + + // ... other methods +} + +const store = new Store({value: 1}); +declare const someExternalData: unknown; + +store.execute(state => ({value: state.value + 1})); +//=> `TPayload` is `void` + +store.execute((state, payload) => ({value: state.value + payload}), 5); +//=> `TPayload` is `5` + +store.execute((state, payload) => ({value: state.value + payload}), someExternalData); +//=> Errors: `action` is `(state: TState) => TState` +``` + +@category Utilities +*/ +export type IsUnknown = ( + unknown extends T // `T` can be `unknown` or `any` + ? IsNull extends false // `any` can be `null`, but `unknown` can't be + ? true + : false + : false +); diff --git a/source/jsonify.d.ts b/source/jsonify.d.ts index 1dc8780df..a9039058a 100644 --- a/source/jsonify.d.ts +++ b/source/jsonify.d.ts @@ -1,8 +1,9 @@ import type {JsonPrimitive, JsonValue} from './basic'; import type {EmptyObject} from './empty-object'; -import type {IsAny, UndefinedToOptional} from './internal'; +import type {UndefinedToOptional} from './internal'; import type {NegativeInfinity, PositiveInfinity} from './numeric'; import type {TypedArray} from './typed-array'; +import type {IsAny} from './is-any'; // Note: The return value has to be `any` and not `unknown` so it can match `void`. type NotJsonable = ((...arguments_: any[]) => any) | undefined | symbol; diff --git a/source/set-return-type.d.ts b/source/set-return-type.d.ts index 8adc46a5f..3081a843e 100644 --- a/source/set-return-type.d.ts +++ b/source/set-return-type.d.ts @@ -1,4 +1,4 @@ -import type {IsUnknown} from './internal'; +import type {IsUnknown} from './is-unknown'; /** Create a function type with a return type of your choice and the same parameters as the given function type. diff --git a/test-d/if-any.ts b/test-d/if-any.ts new file mode 100644 index 000000000..972cdbc60 --- /dev/null +++ b/test-d/if-any.ts @@ -0,0 +1,13 @@ +import {expectError, expectType} from 'tsd'; +import type {IfAny} from '../index'; + +declare const _any: any; + +// `IfAny` should return `true`/`false` if only `T` is specified +expectType>(true); +expectType>(false); +expectType>('T'); +expectType>('F'); + +// Missing generic parameter +expectError(_any); diff --git a/test-d/if-never.ts b/test-d/if-never.ts new file mode 100644 index 000000000..724d9d831 --- /dev/null +++ b/test-d/if-never.ts @@ -0,0 +1,13 @@ +import {expectError, expectType} from 'tsd'; +import type {IfNever} from '../index'; + +declare const _never: never; + +// `IfNever` should return `true`/`false` if only `T` is specified +expectType>(true); +expectType>(false); +expectType>('T'); +expectType>('F'); + +// Missing generic parameter +expectError(_never); diff --git a/test-d/if-unknown.ts b/test-d/if-unknown.ts new file mode 100644 index 000000000..a11a5ce6f --- /dev/null +++ b/test-d/if-unknown.ts @@ -0,0 +1,13 @@ +import {expectError, expectType} from 'tsd'; +import type {IfUnknown} from '../index'; + +declare const _unknown: unknown; + +// `IfUnknown` should return `true`/`false` if only `T` is specified +expectType>(true); +expectType>(false); +expectType>('T'); +expectType>('F'); + +// Missing generic parameter +expectError(_unknown); diff --git a/test-d/internal.ts b/test-d/internal.ts index 631966a54..b98e9e0a3 100644 --- a/test-d/internal.ts +++ b/test-d/internal.ts @@ -1,5 +1,10 @@ import {expectType} from 'tsd'; -import type {IsWhitespace, IsNumeric, IsNotFalse} from '../source/internal'; +import type { + IsWhitespace, + IsNumeric, + IsNotFalse, + IsNull, +} from '../source/internal'; expectType>(false); expectType>(true); @@ -35,3 +40,12 @@ expectType>(true); expectType>(false); expectType>(false); expectType>(false); + +// 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/is-any.ts b/test-d/is-any.ts new file mode 100644 index 000000000..97ca8e17f --- /dev/null +++ b/test-d/is-any.ts @@ -0,0 +1,19 @@ +import {expectError, expectType} from 'tsd'; +import type {IsAny} from '../index'; + +declare const anything: any; +declare const something = 'something'; + +// `IsAny` should only be true for `any` +expectType>(true); +expectType>(true); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); + +// Missing generic parameter +expectError(anything); diff --git a/test-d/is-never.ts b/test-d/is-never.ts new file mode 100644 index 000000000..76e7e5c6a --- /dev/null +++ b/test-d/is-never.ts @@ -0,0 +1,19 @@ +import {expectError, expectType} from 'tsd'; +import type {IsNever} from '../index'; + +declare const _never: never; +declare const something = 'something'; + +// `IsNever` should only be true for `any` +expectType>(true); +expectType>(true); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); + +// Missing generic parameter +expectError(_never); diff --git a/test-d/is-unknown.ts b/test-d/is-unknown.ts new file mode 100644 index 000000000..6ae78589c --- /dev/null +++ b/test-d/is-unknown.ts @@ -0,0 +1,19 @@ +import {expectError, expectType} from 'tsd'; +import type {IsUnknown} from '../index'; + +declare const _unknown: unknown; +declare const something = 'something'; + +// `IsUnknown` should only be true for `any` +expectType>(true); +expectType>(true); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); +expectType>(false); + +// Missing generic parameter +expectError(_unknown);