diff --git a/readme.md b/readme.md index bb744e7..2d55818 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) - Actively maintained - 2 million weekly downloads @@ -37,6 +38,15 @@ is.number(6); //=> true ``` +[Assertions](#type-assertions) perform the same type checks, but throw errors if the type does not match. + +```js +const {assert} = require('@sindresorhus/is'); + +assert.string(foo); +// foo is known to be a string here +``` + ## API @@ -425,6 +435,30 @@ padLeft('🦄', '🌈'); //=> '🌈🦄' ``` +## Type assertions + +The type guards are also available as [typescript assertions](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#assertion-functions), which throw an error for unexpected types. It is a convenient one-line version of the often repetitive "if-not-expected-type-throw" pattern. + +```ts +import {assert} from '@sindresorhus/is'; + +const handleMovieRatingApiResponse = (response: unknown) => { + assert.plainObject(response); + // `response` is typed as a plain `object` with `unknown` properties + assert.number(response.rating); + // `response.rating` is typed as a `number` + assert.string(response.title); + // `response.title` is typed as a `string` + + return `${response.title} (${response.rating * 10})`; +}; + +handleMovieRatingApiResponse({rating: 0.87, title: 'The Matrix'}); +//=> 'The Matrix (8.7)' + +handleMovieRatingApiResponse({status: 'system maintenance'}); +//=> throws error +``` ## FAQ diff --git a/source/index.ts b/source/index.ts index 94ba314..36004d2 100644 --- a/source/index.ts +++ b/source/index.ts @@ -385,6 +385,231 @@ const predicateOnArray = (method: ArrayMethod, predicate: Predicate, values: unk is.any = (predicate: Predicate, ...values: unknown[]): boolean => predicateOnArray(Array.prototype.some, predicate, values); is.all = (predicate: Predicate, ...values: unknown[]): boolean => predicateOnArray(Array.prototype.every, predicate, values); +const assertType = (condition: boolean, description: string, value: unknown): asserts condition => { + if (!condition) { + throw new TypeError(`Expected value which is "${description}", received value of type ${is(value)}.`); + } +}; + +export const enum AssertionTypeDescription { + class_ = 'Class', + numericString = 'string with a number', + nullOrUndefined = 'null or undefined', + iterable = 'Iterable', + asyncIterable = 'AsyncIterable', + nativePromise = 'native Promise', + urlString = 'string with a URL', + truthy = 'truthy', + falsy = 'falsy', + nan = 'NaN', + primitive = 'primitive', + integer = 'integer', + safeInteger = 'integer', + plainObject = 'plain object', + arrayLike = 'array-like', + typedArray = 'TypedArray', + domElement = 'Element', + nodeStream = 'Node.js Stream', + infinite = 'infinite number', + emptyArray = 'empty array', + nonEmptyArray = 'non-empty array', + emptyString = 'empty string', + nonEmptyString = 'non-empty string', + emptyStringOrWhitespace = 'empty string or whitespace', + emptyObject = 'empty object', + nonEmptyObject = 'non-empty object', + emptySet = 'empty set', + nonEmptySet = 'non-empty set', + emptyMap = 'empty map', + nonEmptyMap = 'non-empty map', + + evenInteger = 'even integer', + oddInteger = 'odd integer', + + directInstanceOf = 'T', + inRange = 'in range', + + any = 'predicate returns truthy for any value', + all = 'predicate returns truthy for all values', +} + +// Type assertions have to be declared with an explicit type. +interface Assert { + // Unknowns. + undefined: (value: unknown) => asserts value is undefined; + string: (value: unknown) => asserts value is string; + number: (value: unknown) => asserts value is number; + bigint: (value: unknown) => asserts value is bigint; + // eslint-disable-next-line @typescript-eslint/ban-types + function_: (value: unknown) => asserts value is Function; + null_: (value: unknown) => asserts value is null; + class_: (value: unknown) => asserts value is Class; + boolean: (value: unknown) => asserts value is boolean; + symbol: (value: unknown) => asserts value is symbol; + numericString: (value: unknown) => asserts value is string; + 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; + iterable: (value: unknown) => asserts value is Iterable; + asyncIterable: (value: unknown) => asserts value is AsyncIterable; + generator: (value: unknown) => asserts value is Generator; + nativePromise: (value: unknown) => asserts value is Promise; + promise: (value: unknown) => asserts value is Promise; + generatorFunction: (value: unknown) => asserts value is GeneratorFunction; + // eslint-disable-next-line @typescript-eslint/ban-types + asyncFunction: (value: unknown) => asserts value is Function; + // eslint-disable-next-line @typescript-eslint/ban-types + boundFunction: (value: unknown) => asserts value is Function; + 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; + set: (value: unknown) => asserts value is Set; + 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; + uint8ClampedArray: (value: unknown) => asserts value is Uint8ClampedArray; + int16Array: (value: unknown) => asserts value is Int16Array; + uint16Array: (value: unknown) => asserts value is Uint16Array; + int32Array: (value: unknown) => asserts value is Int32Array; + uint32Array: (value: unknown) => asserts value is Uint32Array; + float32Array: (value: unknown) => asserts value is Float32Array; + float64Array: (value: unknown) => asserts value is Float64Array; + bigInt64Array: (value: unknown) => asserts value is BigInt64Array; + bigUint64Array: (value: unknown) => asserts value is BigUint64Array; + arrayBuffer: (value: unknown) => asserts value is ArrayBuffer; + sharedArrayBuffer: (value: unknown) => asserts value is SharedArrayBuffer; + dataView: (value: unknown) => asserts value is DataView; + urlInstance: (value: unknown) => asserts value is URL; + urlString: (value: unknown) => asserts value is string; + truthy: (value: unknown) => asserts value is unknown; + falsy: (value: unknown) => asserts value is unknown; + nan: (value: unknown) => asserts value is unknown; + 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}; + typedArray: (value: unknown) => asserts value is TypedArray; + arrayLike: (value: unknown) => asserts value is ArrayLike; + domElement: (value: unknown) => asserts value is Element; + observable: (value: unknown) => asserts value is ObservableLike; + nodeStream: (value: unknown) => asserts value is NodeStream; + infinite: (value: unknown) => asserts value is number; + emptyArray: (value: unknown) => asserts value is never[]; + nonEmptyArray: (value: unknown) => asserts value is unknown[]; + 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}; + emptySet: (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; + + // Numbers. + evenInteger: (value: number) => asserts value is number; + oddInteger: (value: number) => asserts value is number; + + // Two arguments. + directInstanceOf: (instance: unknown, class_: Class) => asserts instance is T; + inRange: (value: number, range: number | number[]) => asserts value is number; + + // Variadic functions. + any: (predicate: Predicate, ...values: unknown[]) => void | never; + all: (predicate: Predicate, ...values: unknown[]) => void | never; +} + +export const assert: Assert = { + // Unknowns. + undefined: (value: unknown): asserts value is undefined => assertType(is.undefined(value), TypeName.undefined, value), + string: (value: unknown): asserts value is string => assertType(is.string(value), TypeName.string, value), + number: (value: unknown): asserts value is number => assertType(is.number(value), TypeName.number, value), + bigint: (value: unknown): asserts value is bigint => assertType(is.bigint(value), TypeName.bigint, value), + // eslint-disable-next-line @typescript-eslint/ban-types + function_: (value: unknown): asserts value is Function => assertType(is.function_(value), TypeName.Function, value), + null_: (value: unknown): asserts value is null => assertType(is.null_(value), TypeName.null, value), + class_: (value: unknown): asserts value is Class => assertType(is.class_(value), AssertionTypeDescription.class_, value), + boolean: (value: unknown): asserts value is boolean => assertType(is.boolean(value), TypeName.boolean, value), + symbol: (value: unknown): asserts value is symbol => assertType(is.symbol(value), TypeName.symbol, value), + numericString: (value: unknown): asserts value is string => assertType(is.numericString(value), AssertionTypeDescription.numericString, value), + 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), + 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), + nativePromise: (value: unknown): asserts value is Promise => assertType(is.nativePromise(value), AssertionTypeDescription.nativePromise, value), + promise: (value: unknown): asserts value is Promise => assertType(is.promise(value), TypeName.Promise, value), + generatorFunction: (value: unknown): asserts value is GeneratorFunction => assertType(is.generatorFunction(value), TypeName.GeneratorFunction, value), + // eslint-disable-next-line @typescript-eslint/ban-types + asyncFunction: (value: unknown): asserts value is Function => assertType(is.asyncFunction(value), TypeName.AsyncFunction, value), + // eslint-disable-next-line @typescript-eslint/ban-types + boundFunction: (value: unknown): asserts value is Function => assertType(is.boundFunction(value), TypeName.Function, value), + 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), + 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), + 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), + uint8ClampedArray: (value: unknown): asserts value is Uint8ClampedArray => assertType(is.uint8ClampedArray(value), TypeName.Uint8ClampedArray, value), + int16Array: (value: unknown): asserts value is Int16Array => assertType(is.int16Array(value), TypeName.Int16Array, value), + uint16Array: (value: unknown): asserts value is Uint16Array => assertType(is.uint16Array(value), TypeName.Uint16Array, value), + int32Array: (value: unknown): asserts value is Int32Array => assertType(is.int32Array(value), TypeName.Int32Array, value), + uint32Array: (value: unknown): asserts value is Uint32Array => assertType(is.uint32Array(value), TypeName.Uint32Array, value), + float32Array: (value: unknown): asserts value is Float32Array => assertType(is.float32Array(value), TypeName.Float32Array, value), + float64Array: (value: unknown): asserts value is Float64Array => assertType(is.float64Array(value), TypeName.Float64Array, value), + bigInt64Array: (value: unknown): asserts value is BigInt64Array => assertType(is.bigInt64Array(value), TypeName.BigInt64Array, value), + bigUint64Array: (value: unknown): asserts value is BigUint64Array => assertType(is.bigUint64Array(value), TypeName.BigUint64Array, value), + arrayBuffer: (value: unknown): asserts value is ArrayBuffer => assertType(is.arrayBuffer(value), TypeName.ArrayBuffer, value), + sharedArrayBuffer: (value: unknown): asserts value is SharedArrayBuffer => assertType(is.sharedArrayBuffer(value), TypeName.SharedArrayBuffer, value), + dataView: (value: unknown): asserts value is DataView => assertType(is.dataView(value), TypeName.DataView, value), + urlInstance: (value: unknown): asserts value is URL => assertType(is.urlInstance(value), TypeName.URL, value), + urlString: (value: unknown): asserts value is string => assertType(is.urlString(value), AssertionTypeDescription.urlString, value), + truthy: (value: unknown): asserts value is unknown => assertType(is.truthy(value), AssertionTypeDescription.truthy, value), + falsy: (value: unknown): asserts value is unknown => assertType(is.falsy(value), AssertionTypeDescription.falsy, value), + nan: (value: unknown): asserts value is unknown => assertType(is.nan(value), AssertionTypeDescription.nan, value), + 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), + 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), + observable: (value: unknown): asserts value is ObservableLike => assertType(is.observable(value), TypeName.Observable, value), + nodeStream: (value: unknown): asserts value is NodeStream => assertType(is.nodeStream(value), AssertionTypeDescription.nodeStream, value), + infinite: (value: unknown): asserts value is number => assertType(is.infinite(value), AssertionTypeDescription.infinite, value), + emptyArray: (value: unknown): asserts value is never[] => assertType(is.emptyArray(value), AssertionTypeDescription.emptyArray, value), + nonEmptyArray: (value: unknown): asserts value is unknown[] => assertType(is.nonEmptyArray(value), AssertionTypeDescription.nonEmptyArray, value), + 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), + 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), + 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), + + // Numbers. + evenInteger: (value: number): asserts value is number => assertType(is.evenInteger(value), AssertionTypeDescription.evenInteger, value), + oddInteger: (value: number): asserts value is number => assertType(is.oddInteger(value), AssertionTypeDescription.oddInteger, value), + + // Two arguments. + directInstanceOf: (instance: unknown, class_: Class): asserts instance is T => assertType(is.directInstanceOf(instance, class_), AssertionTypeDescription.directInstanceOf, instance), + inRange: (value: number, range: number | number[]): asserts value is number => assertType(is.inRange(value, range), AssertionTypeDescription.inRange, value), + + // Variadic functions. + any: (predicate: Predicate, ...values: unknown[]): void | never => assertType(is.any(predicate, ...values), AssertionTypeDescription.any, values), + all: (predicate: Predicate, ...values: unknown[]): void | never => assertType(is.all(predicate, ...values), AssertionTypeDescription.all, values) +}; + // Some few keywords are reserved, but we'll populate them for Node.js users // See https://github.com/Microsoft/TypeScript/issues/2536 Object.defineProperties(is, { @@ -398,9 +623,21 @@ Object.defineProperties(is, { value: is.null_ } }); +Object.defineProperties(assert, { + class: { + value: assert.class_ + }, + function: { + value: assert.function_ + }, + null: { + value: assert.null_ + } +}); export default is; // For CommonJS default export support module.exports = is; module.exports.default = is; +module.exports.assert = assert; diff --git a/test/test.ts b/test/test.ts index b7d017d..6e6c4be 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,13 +1,14 @@ -import fs = require('fs'); -import net = require('net'); -import Stream = require('stream'); import {inspect} from 'util'; -import tempy = require('tempy'); import test, {ExecutionContext} from 'ava'; import {JSDOM} from 'jsdom'; import {Subject, Observable} from 'rxjs'; +import is, {assert, AssertionTypeDescription, TypeName} from '../source'; + +import fs = require('fs'); +import net = require('net'); +import Stream = require('stream'); +import tempy = require('tempy'); import ZenObservable = require('zen-observable'); -import is, {TypeName} from '../source'; class PromiseSubclassFixture extends Promise {} class ErrorSubclassFixture extends Error {} @@ -17,14 +18,33 @@ const {document} = window; const createDomElement = (element: string) => document.createElement(element); interface Test { + assert: (...args: any[]) => void | never; fixtures: unknown[]; typename?: TypeName; + typeDescription?: AssertionTypeDescription | TypeName; is(value: unknown): boolean; } +const invertAssertThrow = (description: string, fn: () => void | never, value: unknown): void | never => { + const expectedAssertErrorMessage = `Expected value which is "${description}", received value of type ${is(value)}.`; + + try { + fn(); + } catch (error) { + if (error instanceof TypeError && error.message.includes(expectedAssertErrorMessage)) { + return; + } + + throw error; + } + + throw new Error(`Function did not throw any error, expected: ${expectedAssertErrorMessage}`); +}; + const types = new Map([ ['undefined', { is: is.undefined, + assert: assert.undefined, fixtures: [ undefined ], @@ -32,6 +52,7 @@ const types = new Map([ }], ['null', { is: is.null_, + assert: assert.null_, fixtures: [ null ], @@ -39,6 +60,7 @@ const types = new Map([ }], ['string', { is: is.string, + assert: assert.string, fixtures: [ '🦄', 'hello world', @@ -48,14 +70,17 @@ const types = new Map([ }], ['emptyString', { is: is.emptyString, + assert: assert.emptyString, fixtures: [ '', String() ], - typename: TypeName.string + typename: TypeName.string, + typeDescription: AssertionTypeDescription.emptyString }], ['number', { is: is.number, + assert: assert.number, fixtures: [ 6, 1.4, @@ -68,6 +93,7 @@ const types = new Map([ }], ['bigint', { is: is.bigint, + assert: assert.bigint, fixtures: [ // Disabled until TS supports it for an ESnnnn target. // 1n, @@ -79,6 +105,7 @@ const types = new Map([ }], ['boolean', { is: is.boolean, + assert: assert.boolean, fixtures: [ true, false ], @@ -86,6 +113,7 @@ const types = new Map([ }], ['symbol', { is: is.symbol, + assert: assert.symbol, fixtures: [ Symbol('🦄') ], @@ -93,15 +121,18 @@ const types = new Map([ }], ['numericString', { is: is.numericString, + assert: assert.numericString, fixtures: [ '5', '-3.2', 'Infinity' ], - typename: TypeName.string + typename: TypeName.string, + typeDescription: AssertionTypeDescription.numericString }], ['array', { is: is.array, + assert: assert.array, fixtures: [ [1, 2], new Array(2) @@ -110,14 +141,17 @@ const types = new Map([ }], ['emptyArray', { is: is.emptyArray, + assert: assert.emptyArray, fixtures: [ [], new Array() // eslint-disable-line @typescript-eslint/no-array-constructor ], - typename: TypeName.Array + typename: TypeName.Array, + typeDescription: AssertionTypeDescription.emptyArray }], ['function', { is: is.function_, + assert: assert.function_, fixtures: [ function foo() {}, // eslint-disable-line func-names function () {}, @@ -129,6 +163,7 @@ const types = new Map([ }], ['buffer', { is: is.buffer, + assert: assert.buffer, fixtures: [ Buffer.from('🦄') ], @@ -136,6 +171,7 @@ const types = new Map([ }], ['object', { is: is.object, + assert: assert.object, fixtures: [ {x: 1}, Object.create({x: 1}) @@ -144,6 +180,7 @@ const types = new Map([ }], ['regExp', { is: is.regExp, + assert: assert.regExp, fixtures: [ /\w/, new RegExp('\\w') // eslint-disable-line prefer-regex-literals @@ -152,6 +189,7 @@ const types = new Map([ }], ['date', { is: is.date, + assert: assert.date, fixtures: [ new Date() ], @@ -159,6 +197,7 @@ const types = new Map([ }], ['error', { is: is.error, + assert: assert.error, fixtures: [ new Error('🦄'), new ErrorSubclassFixture() @@ -167,21 +206,26 @@ const types = new Map([ }], ['nativePromise', { is: is.nativePromise, + assert: assert.nativePromise, fixtures: [ Promise.resolve(), PromiseSubclassFixture.resolve() ], - typename: TypeName.Promise + typename: TypeName.Promise, + typeDescription: AssertionTypeDescription.nativePromise }], ['promise', { is: is.promise, + assert: assert.promise, fixtures: [ {then() {}, catch() {}} ], - typename: TypeName.Object + typename: TypeName.Object, + typeDescription: TypeName.Promise }], ['generator', { is: is.generator, + assert: assert.generator, fixtures: [ (function * () { yield 4; @@ -191,23 +235,28 @@ const types = new Map([ }], ['generatorFunction', { is: is.generatorFunction, + assert: assert.generatorFunction, fixtures: [ function * () { yield 4; } ], - typename: TypeName.Function + typename: TypeName.Function, + typeDescription: TypeName.GeneratorFunction }], ['asyncFunction', { is: is.asyncFunction, + assert: assert.asyncFunction, fixtures: [ async function () {}, async () => {} ], - typename: TypeName.Function + typename: TypeName.Function, + typeDescription: TypeName.AsyncFunction }], ['boundFunction', { is: is.boundFunction, + assert: assert.boundFunction, fixtures: [ () => {}, function () {}.bind(null) // eslint-disable-line no-extra-bind @@ -216,6 +265,7 @@ const types = new Map([ }], ['map', { is: is.map, + assert: assert.map, fixtures: [ new Map([['one', '1']]) ], @@ -223,13 +273,16 @@ const types = new Map([ }], ['emptyMap', { is: is.emptyMap, + assert: assert.emptyMap, fixtures: [ new Map() ], - typename: TypeName.Map + typename: TypeName.Map, + typeDescription: AssertionTypeDescription.emptyMap }], ['set', { is: is.set, + assert: assert.set, fixtures: [ new Set(['one']) ], @@ -237,13 +290,16 @@ const types = new Map([ }], ['emptySet', { is: is.emptySet, + assert: assert.emptySet, fixtures: [ new Set() ], - typename: TypeName.Set + typename: TypeName.Set, + typeDescription: AssertionTypeDescription.emptySet }], ['weakSet', { is: is.weakSet, + assert: assert.weakSet, fixtures: [ new WeakSet() ], @@ -251,6 +307,7 @@ const types = new Map([ }], ['weakMap', { is: is.weakMap, + assert: assert.weakMap, fixtures: [ new WeakMap() ], @@ -258,6 +315,7 @@ const types = new Map([ }], ['int8Array', { is: is.int8Array, + assert: assert.int8Array, fixtures: [ new Int8Array() ], @@ -265,6 +323,7 @@ const types = new Map([ }], ['uint8Array', { is: is.uint8Array, + assert: assert.uint8Array, fixtures: [ new Uint8Array() ], @@ -272,6 +331,7 @@ const types = new Map([ }], ['uint8ClampedArray', { is: is.uint8ClampedArray, + assert: assert.uint8ClampedArray, fixtures: [ new Uint8ClampedArray() ], @@ -279,6 +339,7 @@ const types = new Map([ }], ['int16Array', { is: is.int16Array, + assert: assert.int16Array, fixtures: [ new Int16Array() ], @@ -286,6 +347,7 @@ const types = new Map([ }], ['uint16Array', { is: is.uint16Array, + assert: assert.uint16Array, fixtures: [ new Uint16Array() ], @@ -293,6 +355,7 @@ const types = new Map([ }], ['int32Array', { is: is.int32Array, + assert: assert.int32Array, fixtures: [ new Int32Array() ], @@ -300,6 +363,7 @@ const types = new Map([ }], ['uint32Array', { is: is.uint32Array, + assert: assert.uint32Array, fixtures: [ new Uint32Array() ], @@ -307,6 +371,7 @@ const types = new Map([ }], ['float32Array', { is: is.float32Array, + assert: assert.float32Array, fixtures: [ new Float32Array() ], @@ -314,6 +379,7 @@ const types = new Map([ }], ['float64Array', { is: is.float64Array, + assert: assert.float64Array, fixtures: [ new Float64Array() ], @@ -321,6 +387,7 @@ const types = new Map([ }], ['bigInt64Array', { is: is.bigInt64Array, + assert: assert.bigInt64Array, fixtures: [ new BigInt64Array() ], @@ -328,6 +395,7 @@ const types = new Map([ }], ['bigUint64Array', { is: is.bigUint64Array, + assert: assert.bigUint64Array, fixtures: [ new BigUint64Array() ], @@ -335,6 +403,7 @@ const types = new Map([ }], ['arrayBuffer', { is: is.arrayBuffer, + assert: assert.arrayBuffer, fixtures: [ new ArrayBuffer(10) ], @@ -342,6 +411,7 @@ const types = new Map([ }], ['dataView', { is: is.dataView, + assert: assert.dataView, fixtures: [ new DataView(new ArrayBuffer(10)) ], @@ -349,45 +419,56 @@ const types = new Map([ }], ['nan', { is: is.nan, + assert: assert.nan, fixtures: [ NaN, Number.NaN ], - typename: TypeName.number + typename: TypeName.number, + typeDescription: AssertionTypeDescription.nan }], ['nullOrUndefined', { is: is.nullOrUndefined, + assert: assert.nullOrUndefined, fixtures: [ null, undefined - ] + ], + typeDescription: AssertionTypeDescription.nullOrUndefined }], ['plainObject', { is: is.plainObject, + assert: assert.plainObject, fixtures: [ {x: 1}, Object.create(null), new Object() // eslint-disable-line no-new-object ], - typename: TypeName.Object + typename: TypeName.Object, + typeDescription: AssertionTypeDescription.plainObject }], ['integer', { is: is.integer, + assert: assert.integer, fixtures: [ 6 ], - typename: TypeName.number + typename: TypeName.number, + typeDescription: AssertionTypeDescription.integer }], ['safeInteger', { is: is.safeInteger, + assert: assert.safeInteger, fixtures: [ (2 ** 53) - 1, -(2 ** 53) + 1 ], - typename: TypeName.number + typename: TypeName.number, + typeDescription: AssertionTypeDescription.safeInteger }], ['domElement', { is: is.domElement, + assert: assert.domElement, fixtures: [ 'div', 'input', @@ -395,10 +476,12 @@ const types = new Map([ 'img', 'canvas', 'script' - ].map(createDomElement) + ].map(createDomElement), + typeDescription: AssertionTypeDescription.domElement }], ['non-domElements', { is: value => !is.domElement(value), + assert: (value: unknown) => invertAssertThrow(AssertionTypeDescription.domElement, () => assert.domElement(value), value), fixtures: [ document.createTextNode('data'), document.createProcessingInstruction('xml-stylesheet', 'href="mycss.css" type="text/css"'), @@ -410,6 +493,7 @@ const types = new Map([ }], ['observable', { is: is.observable, + assert: assert.observable, fixtures: [ new Observable(), new Subject(), @@ -419,6 +503,7 @@ const types = new Map([ }], ['nodeStream', { is: is.nodeStream, + assert: assert.nodeStream, fixtures: [ fs.createReadStream('readme.md'), fs.createWriteStream(tempy.file()), @@ -430,15 +515,18 @@ const types = new Map([ new Stream.Stream(), new Stream.Writable() ], - typename: TypeName.Object + typename: TypeName.Object, + typeDescription: AssertionTypeDescription.nodeStream }], ['infinite', { is: is.infinite, + assert: assert.infinite, fixtures: [ Infinity, -Infinity ], - typename: TypeName.number + typename: TypeName.number, + typeDescription: AssertionTypeDescription.infinite }] ]); @@ -452,7 +540,7 @@ const testType = (t: ExecutionContext, type: string, exclude?: string[]) => { return; } - const {is: testIs, typename} = testData; + const {is: testIs, assert: testAssert, typename, typeDescription} = testData; for (const [key, {fixtures}] of types) { // TODO: Automatically exclude value types in other tests that we have in the current one. @@ -462,10 +550,13 @@ const testType = (t: ExecutionContext, type: string, exclude?: string[]) => { } const isTypeUnderTest = key === type; - const assert = isTypeUnderTest ? t.true.bind(t) : t.false.bind(t); + const assertIs = isTypeUnderTest ? t.true.bind(t) : t.false.bind(t); + const assertAsserts = isTypeUnderTest ? t.notThrows.bind(t) : t.throws.bind(t); for (const fixture of fixtures) { - assert(testIs(fixture), `Value: ${inspect(fixture)}`); + assertIs(testIs(fixture), `Value: ${inspect(fixture)}`); + const valueType = JSON.stringify(typeDescription ? typeDescription : typename); + assertAsserts(() => testAssert(fixture), `Expected value which is ${valueType}, received value of type ${is(fixture)}.`); if (isTypeUnderTest && typename) { t.is(is(fixture), typename); @@ -506,6 +597,8 @@ test('is.numericString', t => { testType(t, 'numericString'); t.false(is.numericString('')); t.false(is.numericString(1)); + t.throws(() => assert.numericString('')); + t.throws(() => assert.numericString(1)); }); test('is.array', t => { @@ -518,6 +611,7 @@ test('is.function', t => { test('is.boundFunction', t => { t.false(is.boundFunction(function () {})); // eslint-disable-line prefer-arrow-callback + t.throws(() => assert.boundFunction(function () {})); // eslint-disable-line prefer-arrow-callback }); test('is.buffer', t => { @@ -535,6 +629,7 @@ test('is.object', t => { for (const el of testData.fixtures) { t.true(is.object(el)); + t.notThrows(() => assert.object(el)); } }); @@ -565,6 +660,8 @@ test('is.asyncFunction', t => { if (is.asyncFunction(fixture)) { // eslint-disable-next-line promise/prefer-await-to-then t.true(is.function_(fixture().then)); + // eslint-disable-next-line promise/prefer-await-to-then + t.notThrows(() => assert.function_(fixture().then)); } }); @@ -650,9 +747,13 @@ test('is.directInstanceOf', t => { t.true(is.directInstanceOf(error, Error)); t.true(is.directInstanceOf(errorSubclass, ErrorSubclassFixture)); + t.notThrows(() => assert.directInstanceOf(error, Error)); + t.notThrows(() => assert.directInstanceOf(errorSubclass, ErrorSubclassFixture)); t.false(is.directInstanceOf(error, ErrorSubclassFixture)); t.false(is.directInstanceOf(errorSubclass, Error)); + t.throws(() => assert.directInstanceOf(error, ErrorSubclassFixture)); + t.throws(() => assert.directInstanceOf(errorSubclass, Error)); }); test('is.urlInstance', t => { @@ -661,6 +762,11 @@ test('is.urlInstance', t => { t.false(is.urlInstance({})); t.false(is.urlInstance(undefined)); t.false(is.urlInstance(null)); + + t.notThrows(() => assert.urlInstance(url)); + t.throws(() => assert.urlInstance({})); + t.throws(() => assert.urlInstance(undefined)); + t.throws(() => assert.urlInstance(null)); }); test('is.urlString', t => { @@ -670,6 +776,12 @@ test('is.urlString', t => { t.false(is.urlString({})); t.false(is.urlString(undefined)); t.false(is.urlString(null)); + + t.notThrows(() => assert.urlString(url)); + t.throws(() => assert.urlString(new URL(url))); + t.throws(() => assert.urlString({})); + t.throws(() => assert.urlString(undefined)); + t.throws(() => assert.urlString(null)); }); test('is.truthy', t => { @@ -682,6 +794,17 @@ test('is.truthy', t => { // Disabled until TS supports it for an ESnnnn target. // t.true(is.truthy(1n)); t.true(is.truthy(BigInt(1))); + + t.notThrows(() => assert.truthy('unicorn')); + t.notThrows(() => assert.truthy('🦄')); + t.notThrows(() => assert.truthy(new Set())); + t.notThrows(() => assert.truthy(Symbol('🦄'))); + t.notThrows(() => assert.truthy(true)); + t.notThrows(() => assert.truthy(1)); + + // Disabled until TS supports it for an ESnnnn target. + // t.notThrows(() => assert.truthy(1n)); + t.notThrows(() => assert.truthy(BigInt(1))); }); test('is.falsy', t => { @@ -694,6 +817,16 @@ test('is.falsy', t => { // Disabled until TS supports it for an ESnnnn target. // t.true(is.falsy(0n)); t.true(is.falsy(BigInt(0))); + + t.notThrows(() => assert.falsy(false)); + t.notThrows(() => assert.falsy(0)); + t.notThrows(() => assert.falsy('')); + t.notThrows(() => assert.falsy(null)); + t.notThrows(() => assert.falsy(undefined)); + t.notThrows(() => assert.falsy(NaN)); + // Disabled until TS supports it for an ESnnnn target. + // t.notThrows(() => assert.falsy(0n)); + t.notThrows(() => assert.falsy(BigInt(0))); }); test('is.nan', t => { @@ -721,18 +854,22 @@ test('is.primitive', t => { for (const el of primitives) { t.true(is.primitive(el)); + t.notThrows(() => assert.primitive(el)); } }); test('is.integer', t => { testType(t, 'integer', ['number', 'safeInteger']); t.false(is.integer(1.4)); + t.throws(() => assert.integer(1.4)); }); test('is.safeInteger', t => { testType(t, 'safeInteger', ['number', 'integer']); t.false(is.safeInteger(2 ** 53)); t.false(is.safeInteger(-(2 ** 53))); + t.throws(() => assert.safeInteger(2 ** 53)); + t.throws(() => assert.safeInteger(-(2 ** 53))); }); test('is.plainObject', t => { @@ -749,6 +886,16 @@ test('is.iterable', t => { t.false(is.iterable(NaN)); t.false(is.iterable(Infinity)); t.false(is.iterable({})); + + t.notThrows(() => assert.iterable('')); + t.notThrows(() => assert.iterable([])); + t.notThrows(() => assert.iterable(new Map())); + t.throws(() => assert.iterable(null)); + t.throws(() => assert.iterable(undefined)); + t.throws(() => assert.iterable(0)); + t.throws(() => assert.iterable(NaN)); + t.throws(() => assert.iterable(Infinity)); + t.throws(() => assert.iterable({})); }); test('is.asyncIterable', t => { @@ -762,6 +909,17 @@ test('is.asyncIterable', t => { t.false(is.asyncIterable(NaN)); t.false(is.asyncIterable(Infinity)); t.false(is.asyncIterable({})); + + t.notThrows(() => assert.asyncIterable({ + [Symbol.asyncIterator]: () => { } + })); + + t.throws(() => assert.asyncIterable(null)); + t.throws(() => assert.asyncIterable(undefined)); + t.throws(() => assert.asyncIterable(0)); + t.throws(() => assert.asyncIterable(NaN)); + t.throws(() => assert.asyncIterable(Infinity)); + t.throws(() => assert.asyncIterable({})); }); test('is.class', t => { @@ -774,6 +932,7 @@ test('is.class', t => { for (const classDeclaration of classDeclarations) { t.true(is.class_(classDeclaration)); + t.notThrows(() => assert.class_(classDeclaration)); } }); @@ -793,11 +952,16 @@ test('is.typedArray', t => { for (const item of typedArrays) { t.true(is.typedArray(item)); + t.notThrows(() => assert.typedArray(item)); } t.false(is.typedArray(new ArrayBuffer(1))); t.false(is.typedArray([])); t.false(is.typedArray({})); + + t.throws(() => assert.typedArray(new ArrayBuffer(1))); + t.throws(() => assert.typedArray([])); + t.throws(() => assert.typedArray({})); }); test('is.arrayLike', t => { @@ -811,6 +975,17 @@ test('is.arrayLike', t => { t.false(is.arrayLike({})); t.false(is.arrayLike(() => {})); t.false(is.arrayLike(new Map())); + + (function () { + t.notThrows(() => assert.arrayLike(arguments)); // eslint-disable-line prefer-rest-params + })(); + + t.notThrows(() => assert.arrayLike([])); + t.notThrows(() => assert.arrayLike('unicorn')); + + t.throws(() => assert.arrayLike({})); + t.throws(() => assert.arrayLike(() => { })); + t.throws(() => assert.arrayLike(new Map())); }); test('is.inRange', t => { @@ -842,11 +1017,39 @@ test('is.inRange', t => { t.throws(() => { is.inRange(0, [1, 2, 3]); }); + + t.notThrows(() => assert.inRange(x, [0, 5])); + t.notThrows(() => assert.inRange(x, [5, 0])); + t.notThrows(() => assert.inRange(x, [-5, 5])); + t.notThrows(() => assert.inRange(x, [5, -5])); + t.throws(() => assert.inRange(x, [4, 8])); + t.notThrows(() => assert.inRange(-7, [-5, -10])); + t.notThrows(() => assert.inRange(-5, [-5, -10])); + t.notThrows(() => assert.inRange(-10, [-5, -10])); + + t.notThrows(() => assert.inRange(x, 10)); + t.notThrows(() => assert.inRange(0, 0)); + t.notThrows(() => assert.inRange(-2, -3)); + t.throws(() => assert.inRange(x, 2)); + t.throws(() => assert.inRange(-3, -2)); + + t.throws(() => { + assert.inRange(0, []); + }); + + t.throws(() => { + assert.inRange(0, [5]); + }); + + t.throws(() => { + assert.inRange(0, [1, 2, 3]); + }); }); test('is.domElement', t => { testType(t, 'domElement'); t.false(is.domElement({nodeType: 1, nodeName: 'div'})); + t.throws(() => assert.domElement({nodeType: 1, nodeName: 'div'})); const htmlTagNameToTypeName = { div: 'HTMLDivElement', @@ -878,20 +1081,24 @@ test('is.infinite', t => { test('is.evenInteger', t => { for (const el of [-6, 2, 4]) { t.true(is.evenInteger(el)); + t.notThrows(() => assert.evenInteger(el)); } for (const el of [-3, 1, 5]) { t.false(is.evenInteger(el)); + t.throws(() => assert.evenInteger(el)); } }); test('is.oddInteger', t => { for (const el of [-5, 7, 13]) { t.true(is.oddInteger(el)); + t.notThrows(() => assert.oddInteger(el)); } for (const el of [-8, 8, 10]) { t.false(is.oddInteger(el)); + t.throws(() => assert.oddInteger(el)); } }); @@ -903,17 +1110,26 @@ test('is.nonEmptyArray', t => { t.true(is.nonEmptyArray([1, 2, 3])); t.false(is.nonEmptyArray([])); t.false(is.nonEmptyArray(new Array())); // eslint-disable-line @typescript-eslint/no-array-constructor + + t.notThrows(() => assert.nonEmptyArray([1, 2, 3])); + t.throws(() => assert.nonEmptyArray([])); + t.throws(() => assert.nonEmptyArray(new Array())); // eslint-disable-line @typescript-eslint/no-array-constructor }); test('is.emptyString', t => { testType(t, 'emptyString', ['string']); t.false(is.emptyString('🦄')); + t.throws(() => assert.emptyString('🦄')); }); test('is.nonEmptyString', t => { t.false(is.nonEmptyString('')); t.false(is.nonEmptyString(String())); t.true(is.nonEmptyString('🦄')); + + t.throws(() => assert.nonEmptyString('')); + t.throws(() => assert.nonEmptyString(String())); + t.notThrows(() => assert.nonEmptyString('🦄')); }); test('is.emptyStringOrWhitespace', t => { @@ -921,12 +1137,20 @@ test('is.emptyStringOrWhitespace', t => { t.true(is.emptyStringOrWhitespace(' ')); t.false(is.emptyStringOrWhitespace('🦄')); t.false(is.emptyStringOrWhitespace('unicorn')); + + t.notThrows(() => assert.emptyStringOrWhitespace(' ')); + t.throws(() => assert.emptyStringOrWhitespace('🦄')); + t.throws(() => assert.emptyStringOrWhitespace('unicorn')); }); test('is.emptyObject', t => { t.true(is.emptyObject({})); t.true(is.emptyObject(new Object())); // eslint-disable-line no-new-object t.false(is.emptyObject({unicorn: '🦄'})); + + t.notThrows(() => assert.emptyObject({})); + t.notThrows(() => assert.emptyObject(new Object())); // eslint-disable-line no-new-object + t.throws(() => assert.emptyObject({unicorn: '🦄'})); }); test('is.nonEmptyObject', t => { @@ -936,6 +1160,10 @@ test('is.nonEmptyObject', t => { t.false(is.nonEmptyObject({})); t.false(is.nonEmptyObject(new Object())); // eslint-disable-line no-new-object t.true(is.nonEmptyObject({unicorn: '🦄'})); + + t.throws(() => assert.nonEmptyObject({})); + t.throws(() => assert.nonEmptyObject(new Object())); // eslint-disable-line no-new-object + t.notThrows(() => assert.nonEmptyObject({unicorn: '🦄'})); }); test('is.emptySet', t => { @@ -945,9 +1173,11 @@ test('is.emptySet', t => { test('is.nonEmptySet', t => { const tempSet = new Set(); t.false(is.nonEmptySet(tempSet)); + t.throws(() => assert.nonEmptySet(tempSet)); tempSet.add(1); t.true(is.nonEmptySet(tempSet)); + t.notThrows(() => assert.nonEmptySet(tempSet)); }); test('is.emptyMap', t => { @@ -957,9 +1187,11 @@ test('is.emptyMap', t => { test('is.nonEmptyMap', t => { const tempMap = new Map(); t.false(is.nonEmptyMap(tempMap)); + t.throws(() => assert.nonEmptyMap(tempMap)); tempMap.set('unicorn', '🦄'); t.true(is.nonEmptyMap(tempMap)); + t.notThrows(() => assert.nonEmptyMap(tempMap)); }); test('is.any', t => { @@ -975,6 +1207,19 @@ test('is.any', t => { t.throws(() => { is.any(is.string); }); + + t.notThrows(() => assert.any(is.string, {}, true, '🦄')); + t.notThrows(() => assert.any(is.object, false, {}, 'unicorns')); + t.throws(() => assert.any(is.boolean, '🦄', [], 3)); + t.throws(() => assert.any(is.integer, true, 'lol', {})); + + t.throws(() => { + assert.any(null as any, true); + }); + + t.throws(() => { + assert.any(is.string); + }); }); test('is.all', t => { @@ -990,4 +1235,56 @@ test('is.all', t => { t.throws(() => { is.all(is.string); }); + + t.notThrows(() => assert.all(is.object, {}, new Set(), new Map())); + t.notThrows(() => assert.all(is.boolean, true, false)); + t.throws(() => assert.all(is.string, '🦄', [])); + t.throws(() => assert.all(is.set, new Map(), {})); + + t.throws(() => { + assert.all(null as any, true); + }); + + t.throws(() => { + assert.all(is.string); + }); +}); + +test('assert', t => { + // Contrived test showing that typescript acknowledges the type assertion in assert.number(). + // Real world usage includes asserting user input, but here we use a random number/string generator. + t.plan(2); + + const getNumberOrStringRandomly = (): number | string => { + const rnd = Math.random(); + + if (rnd < 0.5) { + return 'sometimes this function returns text'; + } + + return rnd; + }; + + const canUseOnlyNumber = (badlyTypedArgument: any): number => { + // Narrow the type to number, or throw an error at runtime for non-numbers. + assert.number(badlyTypedArgument); + + // Both the type and runtime value is number. + return 1000 * badlyTypedArgument; + }; + + const badlyTypedVariable: any = getNumberOrStringRandomly(); + + t.true(is.number(badlyTypedVariable) || is.string(badlyTypedVariable)); + + // Using try/catch for test purposes only. + try { + const result = canUseOnlyNumber(badlyTypedVariable); + + // Got lucky, the input was a number yielding a good result. + t.true(is.number(result)); + } catch { + // Assertion was tripped. + t.true(is.string(badlyTypedVariable)); + } });