diff --git a/readme.md b/readme.md index 45986b8..12a4809 100644 --- a/readme.md +++ b/readme.md @@ -11,6 +11,7 @@ For example, `is.string('🦄') //=> true` - Written in TypeScript - [Extensive use of type guards](#type-guards) - [Supports type assertions](#type-assertions) +- [Aware of generic type parameters](#generic-type-parameters) (use with caution) - Actively maintained - 2 million weekly downloads @@ -466,6 +467,48 @@ handleMovieRatingApiResponse({rating: 0.87, title: 'The Matrix'}); handleMovieRatingApiResponse({rating: '🦄'}); ``` +## Generic type parameters + +The type guards and type assertions are aware of [generic type parameters](https://www.typescriptlang.org/docs/handbook/generics.html), such as `Promise` and `Map`. The default is `unknown` for most cases, since `is` can not check them at runtime. If the generic type is known at compile-time, either implicitly (inferred) or explicitly (provided), `is` propagates the type so it can be used later. + +Use generic type parameters with caution. They are only checked by the TypeScript compiler, and not checked by `is` at runtime. This can lead to unexpected behavior, where the generic type is _assumed_ at compile-time, but actually is something completely different at runtime. It is best to use `unknown` (default) and type-check the value of the generic type parameter at runtime with `is` or `assert`. + +```ts +import { assert } from '@sindresorhus/is'; + +async function badNumberAssumption(input: unknown) { + // Bad assumption about the generic type parameter fools the compile-time type system. + assert.promise(input); + // `input` is a `Promise` but only assumed to be `Promise`. + + const resolved = await input; + // `resolved` is typed as `number` but was not actually checked at runtime. + + // Multiplication will return NaN if the input promise did not actually contain a number. + return 2 * resolved; +} + +async function goodNumberAssertion(input: unknown) { + assert.promise(input); + // `input` is typed as `Promise` + + const resolved = await input; + // `resolved` is typed as `unknown` + + assert.number(resolved); + // `resolved` is typed as `number` + + // Uses runtime checks so only numbers will reach the multiplication. + return 2 * resolved; +} + +badNumberAssumption(Promise.resolve("an unexpected string")); +//=> 'NaN' + +// This correctly throws an error because of the unexpected string value. +goodNumberAssertion(Promise.resolve("an unexpected string")); +``` + ## FAQ ### Why yet another type checking module? diff --git a/source/index.ts b/source/index.ts index a6bc71d..8eb85d2 100644 --- a/source/index.ts +++ b/source/index.ts @@ -135,25 +135,25 @@ is.buffer = (value: unknown): value is Buffer => !is.nullOrUndefined(value) && ! is.nullOrUndefined = (value: unknown): value is null | undefined => is.null_(value) || is.undefined(value); is.object = (value: unknown): value is object => !is.null_(value) && (typeof value === 'object' || is.function_(value)); -is.iterable = (value: unknown): value is IterableIterator => !is.nullOrUndefined(value) && is.function_((value as IterableIterator)[Symbol.iterator]); +is.iterable = (value: unknown): value is IterableIterator => !is.nullOrUndefined(value) && is.function_((value as IterableIterator)[Symbol.iterator]); -is.asyncIterable = (value: unknown): value is AsyncIterableIterator => !is.nullOrUndefined(value) && is.function_((value as AsyncIterableIterator)[Symbol.asyncIterator]); +is.asyncIterable = (value: unknown): value is AsyncIterableIterator => !is.nullOrUndefined(value) && is.function_((value as AsyncIterableIterator)[Symbol.asyncIterator]); is.generator = (value: unknown): value is Generator => is.iterable(value) && is.function_(value.next) && is.function_(value.throw); -is.nativePromise = (value: unknown): value is Promise => - isObjectOfType>(TypeName.Promise)(value); +is.nativePromise = (value: unknown): value is Promise => + isObjectOfType>(TypeName.Promise)(value); -const hasPromiseAPI = (value: unknown): value is Promise => +const hasPromiseAPI = (value: unknown): value is Promise => is.object(value) && - is.function_((value as Promise).then) && // eslint-disable-line promise/prefer-await-to-then - is.function_((value as Promise).catch); + is.function_((value as Promise).then) && // eslint-disable-line promise/prefer-await-to-then + is.function_((value as Promise).catch); -is.promise = (value: unknown): value is Promise => is.nativePromise(value) || hasPromiseAPI(value); +is.promise = (value: unknown): value is Promise => is.nativePromise(value) || hasPromiseAPI(value); is.generatorFunction = isObjectOfType(TypeName.GeneratorFunction); -is.asyncFunction = (value: unknown): value is ((...args: any[]) => Promise) => getObjectType(value) === TypeName.AsyncFunction; +is.asyncFunction = (value: unknown): value is ((...args: any[]) => Promise) => getObjectType(value) === TypeName.AsyncFunction; // eslint-disable-next-line no-prototype-builtins, @typescript-eslint/ban-types is.boundFunction = (value: unknown): value is Function => is.function_(value) && !value.hasOwnProperty('prototype'); @@ -161,9 +161,9 @@ is.boundFunction = (value: unknown): value is Function => is.function_(value) && is.regExp = isObjectOfType(TypeName.RegExp); is.date = isObjectOfType(TypeName.Date); is.error = isObjectOfType(TypeName.Error); -is.map = (value: unknown): value is Map => isObjectOfType>(TypeName.Map)(value); -is.set = (value: unknown): value is Set => isObjectOfType>(TypeName.Set)(value); -is.weakMap = (value: unknown): value is WeakMap => isObjectOfType>(TypeName.WeakMap)(value); +is.map = (value: unknown): value is Map => isObjectOfType>(TypeName.Map)(value); +is.set = (value: unknown): value is Set => isObjectOfType>(TypeName.Set)(value); +is.weakMap = (value: unknown): value is WeakMap => isObjectOfType>(TypeName.WeakMap)(value); is.weakSet = (value: unknown): value is WeakSet => isObjectOfType>(TypeName.WeakSet)(value); is.int8Array = isObjectOfType(TypeName.Int8Array); @@ -230,7 +230,7 @@ is.primitive = (value: unknown): value is Primitive => is.null_(value) || primit is.integer = (value: unknown): value is number => Number.isInteger(value as number); is.safeInteger = (value: unknown): value is number => Number.isSafeInteger(value as number); -is.plainObject = (value: unknown): value is {[key: string]: unknown} => { +is.plainObject = (value: unknown): value is Record => { // From: https://github.com/sindresorhus/is-plain-obj/blob/master/index.js if (getObjectType(value) !== TypeName.Object) { return false; @@ -283,7 +283,7 @@ export interface ArrayLike { } const isValidLength = (value: unknown): value is number => is.safeInteger(value) && value >= 0; -is.arrayLike = (value: unknown): value is ArrayLike => !is.nullOrUndefined(value) && !is.function_(value) && isValidLength((value as ArrayLike).length); +is.arrayLike = (value: unknown): value is ArrayLike => !is.nullOrUndefined(value) && !is.function_(value) && isValidLength((value as ArrayLike).length); is.inRange = (value: number, range: number | number[]): value is number => { if (is.number(range)) { @@ -354,17 +354,17 @@ is.nonEmptyString = (value: unknown): value is string => is.string(value) && val const isWhiteSpaceString = (value: unknown): value is string => is.string(value) && /\S/.test(value) === false; is.emptyStringOrWhitespace = (value: unknown): value is string => is.emptyString(value) || isWhiteSpaceString(value); -is.emptyObject = (value: unknown): value is {[key: string]: never} => is.object(value) && !is.map(value) && !is.set(value) && Object.keys(value).length === 0; +is.emptyObject = (value: unknown): value is Record => is.object(value) && !is.map(value) && !is.set(value) && Object.keys(value).length === 0; // TODO: Use `not` operator here to remove `Map` and `Set` from type guard: // - https://github.com/Microsoft/TypeScript/pull/29317 -is.nonEmptyObject = (value: unknown): value is {[key: string]: unknown} => is.object(value) && !is.map(value) && !is.set(value) && Object.keys(value).length > 0; +is.nonEmptyObject = (value: unknown): value is Record => is.object(value) && !is.map(value) && !is.set(value) && Object.keys(value).length > 0; is.emptySet = (value: unknown): value is Set => is.set(value) && value.size === 0; -is.nonEmptySet = (value: unknown): value is Set => is.set(value) && value.size > 0; +is.nonEmptySet = (value: unknown): value is Set => is.set(value) && value.size > 0; is.emptyMap = (value: unknown): value is Map => is.map(value) && value.size === 0; -is.nonEmptyMap = (value: unknown): value is Map => is.map(value) && value.size > 0; +is.nonEmptyMap = (value: unknown): value is Map => is.map(value) && value.size > 0; export type Predicate = (value: unknown) => boolean; @@ -450,7 +450,7 @@ interface Assert { array: (value: unknown) => asserts value is T[]; buffer: (value: unknown) => asserts value is Buffer; nullOrUndefined: (value: unknown) => asserts value is null | undefined; - object: (value: unknown) => asserts value is Record; + object: (value: unknown) => asserts value is Record; iterable: (value: unknown) => asserts value is Iterable; asyncIterable: (value: unknown) => asserts value is AsyncIterable; generator: (value: unknown) => asserts value is Generator; @@ -464,9 +464,9 @@ interface Assert { regExp: (value: unknown) => asserts value is RegExp; date: (value: unknown) => asserts value is Date; error: (value: unknown) => asserts value is Error; - map: (value: unknown) => asserts value is Map; + map: (value: unknown) => asserts value is Map; set: (value: unknown) => asserts value is Set; - weakMap: (value: unknown) => asserts value is WeakMap; + weakMap: (value: unknown) => asserts value is WeakMap; weakSet: (value: unknown) => asserts value is WeakSet; int8Array: (value: unknown) => asserts value is Int8Array; uint8Array: (value: unknown) => asserts value is Uint8Array; @@ -490,7 +490,7 @@ interface Assert { primitive: (value: unknown) => asserts value is Primitive; integer: (value: unknown) => asserts value is number; safeInteger: (value: unknown) => asserts value is number; - plainObject: (value: unknown) => asserts value is {[key: string]: unknown}; + plainObject: (value: unknown) => asserts value is Record; typedArray: (value: unknown) => asserts value is TypedArray; arrayLike: (value: unknown) => asserts value is ArrayLike; domElement: (value: unknown) => asserts value is Element; @@ -502,12 +502,12 @@ interface Assert { emptyString: (value: unknown) => asserts value is ''; nonEmptyString: (value: unknown) => asserts value is string; emptyStringOrWhitespace: (value: unknown) => asserts value is string; - emptyObject: (value: unknown) => asserts value is {[key: string]: never}; - nonEmptyObject: (value: unknown) => asserts value is {[key: string]: unknown}; + emptyObject: (value: unknown) => asserts value is Record; + nonEmptyObject: (value: unknown) => asserts value is Record; emptySet: (value: unknown) => asserts value is Set; - nonEmptySet: (value: unknown) => asserts value is Set; + nonEmptySet: (value: unknown) => asserts value is Set; emptyMap: (value: unknown) => asserts value is Map; - nonEmptyMap: (value: unknown) => asserts value is Map; + nonEmptyMap: (value: unknown) => asserts value is Map; // Numbers. evenInteger: (value: number) => asserts value is number; @@ -538,7 +538,7 @@ export const assert: Assert = { array: (value: unknown): asserts value is T[] => assertType(is.array(value), TypeName.Array, value), buffer: (value: unknown): asserts value is Buffer => assertType(is.buffer(value), TypeName.Buffer, value), nullOrUndefined: (value: unknown): asserts value is null | undefined => assertType(is.nullOrUndefined(value), AssertionTypeDescription.nullOrUndefined, value), - object: (value: unknown): asserts value is Record => assertType(is.object(value), TypeName.Object, value), + object: (value: unknown): asserts value is object => assertType(is.object(value), TypeName.Object, value), iterable: (value: unknown): asserts value is Iterable => assertType(is.iterable(value), AssertionTypeDescription.iterable, value), asyncIterable: (value: unknown): asserts value is AsyncIterable => assertType(is.asyncIterable(value), AssertionTypeDescription.asyncIterable, value), generator: (value: unknown): asserts value is Generator => assertType(is.generator(value), TypeName.Generator, value), @@ -552,9 +552,9 @@ export const assert: Assert = { regExp: (value: unknown): asserts value is RegExp => assertType(is.regExp(value), TypeName.RegExp, value), date: (value: unknown): asserts value is Date => assertType(is.date(value), TypeName.Date, value), error: (value: unknown): asserts value is Error => assertType(is.error(value), TypeName.Error, value), - map: (value: unknown): asserts value is Map => assertType(is.map(value), TypeName.Map, value), + map: (value: unknown): asserts value is Map => assertType(is.map(value), TypeName.Map, value), set: (value: unknown): asserts value is Set => assertType(is.set(value), TypeName.Set, value), - weakMap: (value: unknown): asserts value is WeakMap => assertType(is.weakMap(value), TypeName.WeakMap, value), + weakMap: (value: unknown): asserts value is WeakMap => assertType(is.weakMap(value), TypeName.WeakMap, value), weakSet: (value: unknown): asserts value is WeakSet => assertType(is.weakSet(value), TypeName.WeakSet, value), int8Array: (value: unknown): asserts value is Int8Array => assertType(is.int8Array(value), TypeName.Int8Array, value), uint8Array: (value: unknown): asserts value is Uint8Array => assertType(is.uint8Array(value), TypeName.Uint8Array, value), @@ -578,7 +578,7 @@ export const assert: Assert = { primitive: (value: unknown): asserts value is Primitive => assertType(is.primitive(value), AssertionTypeDescription.primitive, value), integer: (value: unknown): asserts value is number => assertType(is.integer(value), AssertionTypeDescription.integer, value), safeInteger: (value: unknown): asserts value is number => assertType(is.safeInteger(value), AssertionTypeDescription.safeInteger, value), - plainObject: (value: unknown): asserts value is {[key: string]: unknown} => assertType(is.plainObject(value), AssertionTypeDescription.plainObject, value), + plainObject: (value: unknown): asserts value is Record => assertType(is.plainObject(value), AssertionTypeDescription.plainObject, value), typedArray: (value: unknown): asserts value is TypedArray => assertType(is.typedArray(value), AssertionTypeDescription.typedArray, value), arrayLike: (value: unknown): asserts value is ArrayLike => assertType(is.arrayLike(value), AssertionTypeDescription.arrayLike, value), domElement: (value: unknown): asserts value is Element => assertType(is.domElement(value), AssertionTypeDescription.domElement, value), @@ -590,12 +590,12 @@ export const assert: Assert = { emptyString: (value: unknown): asserts value is '' => assertType(is.emptyString(value), AssertionTypeDescription.emptyString, value), nonEmptyString: (value: unknown): asserts value is string => assertType(is.nonEmptyString(value), AssertionTypeDescription.nonEmptyString, value), emptyStringOrWhitespace: (value: unknown): asserts value is string => assertType(is.emptyStringOrWhitespace(value), AssertionTypeDescription.emptyStringOrWhitespace, value), - emptyObject: (value: unknown): asserts value is {[key: string]: never} => assertType(is.emptyObject(value), AssertionTypeDescription.emptyObject, value), - nonEmptyObject: (value: unknown): asserts value is {[key: string]: unknown} => assertType(is.nonEmptyObject(value), AssertionTypeDescription.nonEmptyObject, value), + emptyObject: (value: unknown): asserts value is Record => assertType(is.emptyObject(value), AssertionTypeDescription.emptyObject, value), + nonEmptyObject: (value: unknown): asserts value is Record => assertType(is.nonEmptyObject(value), AssertionTypeDescription.nonEmptyObject, value), emptySet: (value: unknown): asserts value is Set => assertType(is.emptySet(value), AssertionTypeDescription.emptySet, value), - nonEmptySet: (value: unknown): asserts value is Set => assertType(is.nonEmptySet(value), AssertionTypeDescription.nonEmptySet, value), + nonEmptySet: (value: unknown): asserts value is Set => assertType(is.nonEmptySet(value), AssertionTypeDescription.nonEmptySet, value), emptyMap: (value: unknown): asserts value is Map => assertType(is.emptyMap(value), AssertionTypeDescription.emptyMap, value), - nonEmptyMap: (value: unknown): asserts value is Map => assertType(is.nonEmptyMap(value), AssertionTypeDescription.nonEmptyMap, value), + nonEmptyMap: (value: unknown): asserts value is Map => assertType(is.nonEmptyMap(value), AssertionTypeDescription.nonEmptyMap, value), // Numbers. evenInteger: (value: number): asserts value is number => assertType(is.evenInteger(value), AssertionTypeDescription.evenInteger, value),