diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 734c8eb..33db234 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -10,10 +10,11 @@ jobs: fail-fast: false matrix: node-version: - - 20 + - 24 + - 22 steps: - - uses: actions/checkout@v4 - - uses: actions/setup-node@v4 + - uses: actions/checkout@v6 + - uses: actions/setup-node@v6 with: node-version: ${{ matrix.node-version }} - run: npm install diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..2996cf5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,9 @@ +# Notes + +## Branded types for type guards + +TypeScript type guards narrow in both branches. If `is.integer(n)` returns `value is number` and the input is `number`, the false branch computes `Exclude` = `never`. This makes common patterns like `if (!is.integer(n)) throw; use(n)` fail because `n` becomes `never` after the guard. + +To avoid this, type guard predicates use branded types (e.g., `number & {readonly __brand: 'Integer'}`, `string & {readonly __brand: 'UrlString'}`). A branded subtype ensures the false branch stays the original type (e.g., `Exclude` = `number`). + +Assert functions (`asserts value is T`) don't need branded types since they throw on failure and have no false branch. They use plain types like `asserts value is number`. diff --git a/CLAUDE.md b/CLAUDE.md new file mode 120000 index 0000000..47dc3e3 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1 @@ +AGENTS.md \ No newline at end of file diff --git a/package.json b/package.json index bcbc532..d7a386f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@sindresorhus/is", - "version": "7.0.1", + "version": "8.1.0", "description": "Type check values", "license": "MIT", "repository": "sindresorhus/is", @@ -17,11 +17,11 @@ }, "sideEffects": false, "engines": { - "node": ">=18" + "node": ">=22" }, "scripts": { "build": "del distribution && tsc", - "test": "tsc --noEmit && xo && ava", + "test": "tsc --noEmit && tsc --project test/tsconfig.json --noEmit && xo && node --experimental-transform-types --test test/test.ts", "prepare": "npm run build" }, "files": [ @@ -51,31 +51,26 @@ "typeguards", "types" ], - "devDependencies": { - "@sindresorhus/tsconfig": "^6.0.0", - "@types/jsdom": "^21.1.7", - "@types/node": "^20.14.10", - "@types/zen-observable": "^0.8.7", - "ava": "^6.1.3", - "del-cli": "^5.1.0", - "expect-type": "^0.19.0", - "jsdom": "^24.1.0", - "rxjs": "^7.8.1", - "tempy": "^3.1.0", - "tsimp": "^2.0.11", - "typescript": "^5.5.3", - "xo": "^0.58.0", - "zen-observable": "^0.10.0" + "xo": { + "rules": { + "@typescript-eslint/no-unsafe-enum-comparison": "off", + "@typescript-eslint/no-confusing-void-expression": "off", + "@typescript-eslint/no-unsafe-type-assertion": "off", + "@stylistic/operator-linebreak": "off" + } }, - "ava": { - "environmentVariables": { - "TSIMP_DIAG": "error" - }, - "extensions": { - "ts": "module" - }, - "nodeArguments": [ - "--import=tsimp/import" - ] + "devDependencies": { + "@sindresorhus/tsconfig": "^8.1.0", + "@types/jsdom": "^28.0.1", + "@types/node": "^25.5.2", + "@types/zen-observable": "^0.8.7", + "del-cli": "^7.0.0", + "expect-type": "^1.3.0", + "jsdom": "^29.0.2", + "rxjs": "^7.8.2", + "tempy": "^3.2.0", + "typescript": "6.0.2", + "xo": "^2.0.2", + "zen-observable": "^0.10.0" } } diff --git a/readme.md b/readme.md index 1966a5b..509f75f 100644 --- a/readme.md +++ b/readme.md @@ -130,6 +130,17 @@ is.array(value); // Validate `value` is an array. is.array(value, is.number); // Validate `value` is an array and all of its items are numbers. ``` +##### .arrayOf(predicate) + +Returns a type guard that checks if `value` is an array where every item matches the predicate. Useful for composing with other methods. + +```js +const isStringArray = is.arrayOf(is.string); + +isStringArray(['a', 'b']); //=> true +isStringArray(['a', 1]); //=> false +``` + ##### .function(value) ##### .buffer(value) @@ -463,6 +474,10 @@ function foo() { foo(); ``` +##### .finiteNumber(value) + +Check if `value` is a number and is finite. Excludes `Infinity` and `-Infinity`. + ##### .positiveNumber(value) Check if `value` is a number and is more than 0. @@ -471,6 +486,22 @@ Check if `value` is a number and is more than 0. Check if `value` is a number and is less than 0. +##### .nonNegativeNumber(value) + +Check if `value` is a number and is 0 or more. + +##### .positiveInteger(value) + +Check if `value` is an integer and is more than 0. + +##### .negativeInteger(value) + +Check if `value` is an integer and is less than 0. + +##### .nonNegativeInteger(value) + +Check if `value` is an integer and is 0 or more. + ##### .inRange(value, range) Check if `value` (number) is in the given `range`. The range is an array of two values, lower bound and upper bound, in no specific order. @@ -575,6 +606,31 @@ is.any([is.boolean, is.number], 'unicorns', [], new Map()); //=> false ``` +##### .any(predicate[]) + +Using an array of `predicate[]` without values, returns a combined type guard that checks if a value matches **any** of the predicates: + +```js +const isStringOrNumber = is.any([is.string, is.number]); + +isStringOrNumber('hello'); +//=> true + +isStringOrNumber(123); +//=> true + +isStringOrNumber(true); +//=> false +``` + +This is useful for composing with other methods like `is.optional`: + +```js +is.optional(value, is.any([is.string, is.number])); +``` + +An empty predicate array currently returns a predicate that always returns `false`. This will throw in the next major release. + ##### .all(predicate, ...values) Returns `true` if **all** of the input `values` returns true in the `predicate`: @@ -587,6 +643,54 @@ is.all(is.string, '🦄', [], 'unicorns'); //=> false ``` +##### .all(predicate[]) + +Using an array of `predicate[]` without values, returns a combined type guard that checks if a value matches **all** of the predicates: + +```js +const isArrayAndNonEmpty = is.all([is.array, is.nonEmptyArray]); + +isArrayAndNonEmpty(['hello']); +//=> true + +isArrayAndNonEmpty([]); +//=> false +``` + +This is useful for composing with other methods like `is.optional`: + +```js +is.optional(value, is.all([is.object, is.plainObject])); +``` + +An empty predicate array currently returns a predicate that always returns `true`. This will throw in the next major release. + +##### .optional(value, predicate) + +Returns `true` if `value` is `undefined` or satisfies the given `predicate`. + +```js +is.optional(undefined, is.string); +//=> true + +is.optional('🦄', is.string); +//=> true + +is.optional(123, is.string); +//=> false +``` + +##### .oneOf(values) + +Returns a type guard that checks if `value` is one of the given `values`. Best used with `as const` for precise type narrowing. + +```ts +const isDirection = is.oneOf(['north', 'south', 'east', 'west'] as const); + +isDirection('north'); //=> true +isDirection('up'); //=> false +``` + ##### .validDate(value) Returns `true` if the value is a valid date. @@ -682,6 +786,61 @@ handleMovieRatingApiResponse({rating: 0.87, title: 'The Matrix'}); handleMovieRatingApiResponse({rating: '🦄'}); ``` +### Negative assertion + +Asserts that `value` is not the specified type. Only exact, type-safe negative assertions are exposed. + +Supported assertions: + +- `assert.not.undefined(value)` +- `assert.not.null(value)` +- `assert.not.nullOrUndefined(value)` +- `assert.not.string(value)` +- `assert.not.boolean(value)` +- `assert.not.symbol(value)` +- `assert.not.bigint(value)` +- `assert.not.primitive(value)` + +This intentionally excludes checks that cannot produce a safe TypeScript complement: `number` because `is.number` rejects `NaN`, refinements such as `integer` and `validDate`, and branded structural object checks such as `map` and `date`. Broad object checks such as `object` are also excluded to keep negative assertions limited to primitive and nullish types. + +```ts +import {assert} from '@sindresorhus/is'; + +const value: string | undefined = getValue(); + +assert.not.undefined(value); +// Throws if `value` is `undefined`. Otherwise, `value` is now typed as `string`. +``` + +For `unknown` input, exact negative assertions narrow to the remaining representable type: + +```ts +const value: unknown = getValue(); + +assert.not.nullOrUndefined(value); +// `value` is now typed as non-nullish. + +assert.not.primitive(value); +// `value` is now typed as `object`. +``` + +### Optional assertion + +Asserts that `value` is `undefined` or satisfies the provided `assertion`. + +```ts +import {assert} from '@sindresorhus/is'; + +assert.optional(undefined, assert.string); +// Passes without throwing + +assert.optional('🦄', assert.string); +// Passes without throwing + +assert.optional(123, assert.string); +// Throws: Expected value which is `string`, received value of type `number` +``` + ## 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` cannot 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. diff --git a/source/index.ts b/source/index.ts index e7835cf..6c2f428 100644 --- a/source/index.ts +++ b/source/index.ts @@ -1,16 +1,33 @@ import type { ArrayLike, Class, + EvenInteger, Falsy, + FiniteNumber, + Integer, + NaN as NaNType, + NegativeInfinity, + NegativeInteger, + NegativeNumber, NodeStream, NonEmptyString, + NonNegativeInteger, + NonNegativeNumber, ObservableLike, + OddInteger, Predicate, Primitive, + PositiveInfinity, + PositiveInteger, + PositiveNumber, + SafeInteger, TypedArray, + UrlString, + ValidLength, WeakRef, Whitespace, -} from './types.js'; +} from './types.ts'; +import {keysOf} from './utilities.ts'; // From type-fest. type ExtractFromGlobalConstructors = @@ -20,6 +37,15 @@ type ExtractFromGlobalConstructors = type NodeBuffer = ExtractFromGlobalConstructors<'Buffer'>; +type NumericGuardResult = + ( + unknown extends Input + ? Branded + : Input extends number + ? Branded & Input + : number + ) & Input; + const typedArrayTypeNames = [ 'Int8Array', 'Uint8Array', @@ -97,6 +123,7 @@ function isPrimitiveTypeName(name: unknown): name is PrimitiveTypeName { export type TypeName = ObjectTypeName | PrimitiveTypeName; const assertionTypeDescriptions = [ + 'bound Function', 'positive number', 'negative number', 'Class', @@ -131,7 +158,13 @@ const assertionTypeDescriptions = [ 'non-empty map', 'PropertyKey', 'even integer', + 'finite number', + 'negative integer', + 'non-negative integer', + 'non-negative number', 'odd integer', + 'positive integer', + 'safe integer', 'T', 'in range', 'predicate returns truthy for any value', @@ -148,7 +181,7 @@ export type AssertionTypeDescription = typeof assertionTypeDescriptions[number]; const getObjectType = (value: unknown): ObjectTypeName | undefined => { const objectTypeName = Object.prototype.toString.call(value).slice(8, -1); - if (/HTML\w+Element/.test(objectTypeName) && isHtmlElement(value)) { + if (/HTML\w+Element/v.test(objectTypeName) && isHtmlElement(value)) { return 'HTMLElement'; } @@ -164,6 +197,7 @@ function detect(value: unknown): TypeName { return 'null'; } + // eslint-disable-next-line @typescript-eslint/switch-exhaustiveness-check switch (typeof value) { case 'undefined': { return 'undefined'; @@ -209,11 +243,15 @@ function detect(value: unknown): TypeName { } const tagType = getObjectType(value); - if (tagType) { + if (tagType !== undefined && tagType !== 'Object') { return tagType; } - if (value instanceof String || value instanceof Boolean || value instanceof Number) { + if (hasPromiseApi(value)) { + return 'Promise'; + } + + if (isBoxedPrimitiveObject(value)) { throw new TypeError('Please don\'t use object wrappers for primitive types'); } @@ -224,6 +262,23 @@ function hasPromiseApi(value: unknown): value is Promise { return isFunction((value as Promise)?.then) && isFunction((value as Promise)?.catch); } +function hasBoxedPrimitiveBrand(value: unknown, valueOf: () => unknown): boolean { + try { + // `Object.prototype.toString` can be spoofed via `Symbol.toStringTag`, but the + // boxed primitive `valueOf` methods still enforce the real internal brand. + Reflect.apply(valueOf, value, []); + return true; + } catch { + return false; + } +} + +function isBoxedPrimitiveObject(value: unknown): boolean { + return hasBoxedPrimitiveBrand(value, String.prototype.valueOf) + || hasBoxedPrimitiveBrand(value, Boolean.prototype.valueOf) + || hasBoxedPrimitiveBrand(value, Number.prototype.valueOf); +} + const is = Object.assign( detect, { @@ -232,6 +287,7 @@ const is = Object.assign( array: isArray, arrayBuffer: isArrayBuffer, arrayLike: isArrayLike, + arrayOf: isArrayOf, asyncFunction: isAsyncFunction, asyncGenerator: isAsyncGenerator, asyncGeneratorFunction: isAsyncGeneratorFunction, @@ -258,6 +314,7 @@ const is = Object.assign( error: isError, evenInteger: isEvenInteger, falsy: isFalsy, + finiteNumber: isFiniteNumber, float32Array: isFloat32Array, float64Array: isFloat64Array, formData: isFormData, @@ -275,6 +332,7 @@ const is = Object.assign( map: isMap, nan: isNan, nativePromise: isNativePromise, + negativeInteger: isNegativeInteger, negativeNumber: isNegativeNumber, nodeStream: isNodeStream, nonEmptyArray: isNonEmptyArray, @@ -283,6 +341,8 @@ const is = Object.assign( nonEmptySet: isNonEmptySet, nonEmptyString: isNonEmptyString, nonEmptyStringAndNotWhitespace: isNonEmptyStringAndNotWhitespace, + nonNegativeInteger: isNonNegativeInteger, + nonNegativeNumber: isNonNegativeNumber, null: isNull, nullOrUndefined: isNullOrUndefined, number: isNumber, @@ -290,7 +350,9 @@ const is = Object.assign( object: isObject, observable: isObservable, oddInteger: isOddInteger, + oneOf: isOneOf, plainObject: isPlainObject, + positiveInteger: isPositiveInteger, positiveNumber: isPositiveNumber, primitive: isPrimitive, promise: isPromise, @@ -312,6 +374,7 @@ const is = Object.assign( urlInstance: isUrlInstance, urlSearchParams: isUrlSearchParams, urlString: isUrlString, + optional: isOptional, validDate: isValidDate, validLength: isValidLength, weakMap: isWeakMap, @@ -325,15 +388,85 @@ function isAbsoluteModule2(remainder: 0 | 1) { return (value: unknown): value is number => isInteger(value) && Math.abs(value % 2) === remainder; } -export function isAll(predicate: Predicate, ...values: unknown[]): boolean { - return predicateOnArray(Array.prototype.every, predicate, values); +type TypeGuard = (value: unknown) => value is T; + +function validatePredicateArray(predicateArray: readonly Predicate[], allowEmpty: boolean) { + if (predicateArray.length === 0) { + if (allowEmpty) { + // Next major release: throw for empty predicate arrays to avoid vacuous results. + // throw new TypeError('Invalid predicate array'); + } else { + throw new TypeError('Invalid predicate array'); + } + + return; + } + + for (const predicate of predicateArray) { + validatePredicate(predicate); + } } -export function isAny(predicate: Predicate | Predicate[], ...values: unknown[]): boolean { - const predicates = isArray(predicate) ? predicate : [predicate]; - return predicates.some(singlePredicate => - predicateOnArray(Array.prototype.some, singlePredicate, values), - ); +function validatePredicate(predicate: Predicate) { + if (!isFunction(predicate)) { + throw new TypeError(`Invalid predicate: ${JSON.stringify(predicate)}`); + } +} + +// Predicate factory overloads - return a type guard when called with only predicates +export function isAll(predicates: [TypeGuard]): TypeGuard; +export function isAll(predicates: [TypeGuard, TypeGuard]): TypeGuard; +export function isAll(predicates: [TypeGuard, TypeGuard, TypeGuard]): TypeGuard; +export function isAll(predicates: [TypeGuard, TypeGuard, TypeGuard, TypeGuard]): TypeGuard; +export function isAll(predicates: [TypeGuard, TypeGuard, TypeGuard, TypeGuard, TypeGuard]): TypeGuard; +export function isAll(predicates: ReadonlyArray>): TypeGuard; +export function isAll(predicates: readonly Predicate[]): Predicate; +// Evaluator overload - check if all values match the predicate +export function isAll(predicate: Predicate | readonly Predicate[], ...values: unknown[]): boolean; +export function isAll(predicate: Predicate | readonly Predicate[], ...values: unknown[]): boolean | Predicate { + if (Array.isArray(predicate)) { + const predicateArray = predicate as readonly Predicate[]; + validatePredicateArray(predicateArray, values.length === 0); + + const combinedPredicate = (value: unknown) => predicateArray.every(singlePredicate => singlePredicate(value)); + if (values.length === 0) { + return combinedPredicate; + } + + return predicateOnArray(Array.prototype.every, combinedPredicate, values); + } + + return predicateOnArray(Array.prototype.every, predicate as Predicate, values); +} + +// Predicate factory overloads - return a type guard when called with only predicates +export function isAny(predicates: [TypeGuard]): TypeGuard; +export function isAny(predicates: [TypeGuard, TypeGuard]): TypeGuard; +export function isAny(predicates: [TypeGuard, TypeGuard, TypeGuard]): TypeGuard; +export function isAny(predicates: [TypeGuard, TypeGuard, TypeGuard, TypeGuard]): TypeGuard; +export function isAny(predicates: [TypeGuard, TypeGuard, TypeGuard, TypeGuard, TypeGuard]): TypeGuard; +export function isAny(predicates: ReadonlyArray>): TypeGuard; +export function isAny(predicates: readonly Predicate[]): Predicate; +// Evaluator overload - check if any value matches any predicate +export function isAny(predicate: Predicate | readonly Predicate[], ...values: unknown[]): boolean; +export function isAny(predicate: Predicate | readonly Predicate[], ...values: unknown[]): boolean | Predicate { + if (Array.isArray(predicate)) { + const predicateArray = predicate as readonly Predicate[]; + validatePredicateArray(predicateArray, values.length === 0); + + const combinedPredicate = (value: unknown) => predicateArray.some(singlePredicate => singlePredicate(value)); + if (values.length === 0) { + return combinedPredicate; + } + + return predicateOnArray(Array.prototype.some, combinedPredicate, values); + } + + return predicateOnArray(Array.prototype.some, predicate as Predicate, values); +} + +export function isOptional(value: unknown, predicate: (value: unknown) => value is T): value is T | undefined { + return isUndefined(value) || predicate(value); } export function isArray(value: unknown, assertion?: (value: T) => value is T): value is T[] { @@ -357,6 +490,10 @@ export function isArrayLike(value: unknown): value is ArrayLike return !isNullOrUndefined(value) && !isFunction(value) && isValidLength((value as ArrayLike).length); } +export function isArrayOf(predicate: (value: unknown) => value is T): (value: unknown) => value is T[] { + return (value: unknown): value is T[] => isArray(value) && value.every(element => predicate(element)); +} + export function isAsyncFunction(value: unknown): value is ((...arguments_: any[]) => Promise) { return getObjectType(value) === 'AsyncFunction'; } @@ -393,7 +530,7 @@ export function isBoolean(value: unknown): value is boolean { return value === true || value === false; } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function isBoundFunction(value: unknown): value is Function { return isFunction(value) && !Object.hasOwn(value, 'prototype'); } @@ -402,12 +539,12 @@ export function isBoundFunction(value: unknown): value is Function { Note: [Prefer using `Uint8Array` instead of `Buffer`.](https://sindresorhus.com/blog/goodbye-nodejs-buffer) */ export function isBuffer(value: unknown): value is NodeBuffer { - // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call + // eslint-disable-next-line @typescript-eslint/no-unsafe-return, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access return (value as any)?.constructor?.isBuffer?.(value) ?? false; } export function isClass(value: unknown): value is Class { - return isFunction(value) && value.toString().startsWith('class '); + return isFunction(value) && /^class(?:\s+|\{)/v.test(value.toString()); } export function isDataView(value: unknown): value is DataView { @@ -435,7 +572,7 @@ export function isEmptyMap(value: unknown): value is Map { } export function isEmptyObject(value: unknown): value is Record { - return isObject(value) && !isMap(value) && !isSet(value) && Object.keys(value).length === 0; + return isObject(value) && !isFunction(value) && !isArray(value) && !isMap(value) && !isSet(value) && Object.keys(value).length === 0; } export function isEmptySet(value: unknown): value is Set { @@ -451,15 +588,31 @@ export function isEmptyStringOrWhitespace(value: unknown): value is '' | Whitesp } export function isEnumCase(value: unknown, targetEnum: T): value is T[keyof T] { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - return Object.values(targetEnum as any).includes(value as string); + // Numeric enums have reverse mappings (e.g. `Direction[0] = "Up"`), so their runtime object contains both `{ Up: 0 }` and `{ "0": "Up" }`. Filtering out entries that round-trip like a canonical number and point back to an own property leaves only actual enum member values. + const enumObject = targetEnum as Record; + + return Object.entries(enumObject).some(([key, enumValue]) => { + if (!isString(enumValue)) { + return enumValue === value; + } + + const numericKey = Number(key); + if (Number.isNaN(numericKey) || String(numericKey) !== key) { + return enumValue === value; + } + + return enumValue === value && !(Object.hasOwn(enumObject, enumValue) && enumObject[enumValue] === numericKey); + }); } export function isError(value: unknown): value is Error { + // TODO: Use `Error.isError` when targeting Node.js 24. return getObjectType(value) === 'Error'; } -export function isEvenInteger(value: unknown): value is number { +// For numeric guards, preserve branded narrowing for `unknown`, keep the false branch usable for plain `number`, and still narrow mixed unions to `number`. +export function isEvenInteger(value: Input): value is NumericGuardResult; +export function isEvenInteger(value: unknown): boolean { return isAbsoluteModule2(0)(value); } @@ -468,6 +621,13 @@ export function isFalsy(value: unknown): value is Falsy { return !value; } +export function isFiniteNumber(value: Input): value is NumericGuardResult; +export function isFiniteNumber(value: unknown): boolean { + return Number.isFinite(value); +} + +// TODO: Support detecting Float16Array when targeting Node.js 24. + export function isFloat32Array(value: unknown): value is Float32Array { return getObjectType(value) === 'Float32Array'; } @@ -480,7 +640,7 @@ export function isFormData(value: unknown): value is FormData { return getObjectType(value) === 'FormData'; } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function isFunction(value: unknown): value is Function { return typeof value === 'function'; } @@ -493,11 +653,9 @@ export function isGeneratorFunction(value: unknown): value is GeneratorFunction return getObjectType(value) === 'GeneratorFunction'; } -// eslint-disable-next-line @typescript-eslint/naming-convention -const NODE_TYPE_ELEMENT = 1; +const NODE_TYPE_ELEMENT = 1; // eslint-disable-line @typescript-eslint/naming-convention -// eslint-disable-next-line @typescript-eslint/naming-convention -const DOM_PROPERTIES_TO_CHECK: Array<(keyof HTMLElement)> = [ +const DOM_PROPERTIES_TO_CHECK: Array<(keyof HTMLElement)> = [ // eslint-disable-line @typescript-eslint/naming-convention 'innerHTML', 'ownerDocument', 'style', @@ -513,7 +671,8 @@ export function isHtmlElement(value: unknown): value is HTMLElement { && DOM_PROPERTIES_TO_CHECK.every(property => property in value); } -export function isInfinite(value: unknown): value is number { +export function isInfinite(value: Input): value is NumericGuardResult; +export function isInfinite(value: unknown): boolean { return value === Number.POSITIVE_INFINITY || value === Number.NEGATIVE_INFINITY; } @@ -523,6 +682,10 @@ export function isInRange(value: number, range: number | [number, number]): valu } if (isArray(range) && range.length === 2) { + if (Number.isNaN(range[0]) || Number.isNaN(range[1])) { + throw new TypeError(`Invalid range: ${JSON.stringify(range)}`); + } + return value >= Math.min(...range) && value <= Math.max(...range); } @@ -541,7 +704,8 @@ export function isInt8Array(value: unknown): value is Int8Array { return getObjectType(value) === 'Int8Array'; } -export function isInteger(value: unknown): value is number { +export function isInteger(value: Input): value is NumericGuardResult; +export function isInteger(value: unknown): boolean { return Number.isInteger(value); } @@ -553,7 +717,8 @@ export function isMap(value: unknown): value is return getObjectType(value) === 'Map'; } -export function isNan(value: unknown) { +export function isNan(value: Input): value is NumericGuardResult; +export function isNan(value: unknown): boolean { return Number.isNaN(value); } @@ -561,7 +726,13 @@ export function isNativePromise(value: unknown): value is Promise(value: Input): value is NumericGuardResult; +export function isNegativeInteger(value: unknown): boolean { + return isInteger(value) && value < 0; +} + +export function isNegativeNumber(value: Input): value is NumericGuardResult; +export function isNegativeNumber(value: unknown): boolean { return isNumber(value) && value < 0; } @@ -580,7 +751,7 @@ export function isNonEmptyMap(value: unknown): v // TODO: Use `not` operator here to remove `Map` and `Set` from type guard: // - https://github.com/Microsoft/TypeScript/pull/29317 export function isNonEmptyObject(value: unknown): value is Record { - return isObject(value) && !isMap(value) && !isSet(value) && Object.keys(value).length > 0; + return isObject(value) && !isFunction(value) && !isArray(value) && !isMap(value) && !isSet(value) && Object.keys(value).length > 0; } export function isNonEmptySet(value: unknown): value is Set { @@ -597,12 +768,22 @@ export function isNonEmptyStringAndNotWhitespace(value: unknown): value is NonEm return isString(value) && !isEmptyStringOrWhitespace(value); } -// eslint-disable-next-line @typescript-eslint/ban-types +export function isNonNegativeInteger(value: Input): value is NumericGuardResult; +export function isNonNegativeInteger(value: unknown): boolean { + return isInteger(value) && value >= 0; +} + +export function isNonNegativeNumber(value: Input): value is NumericGuardResult; +export function isNonNegativeNumber(value: unknown): boolean { + return isNumber(value) && value >= 0; +} + +// eslint-disable-next-line @typescript-eslint/no-restricted-types export function isNull(value: unknown): value is null { return value === null; } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-restricted-types export function isNullOrUndefined(value: unknown): value is null | undefined { return isNull(value) || isUndefined(value); } @@ -612,10 +793,10 @@ export function isNumber(value: unknown): value is number { } export function isNumericString(value: unknown): value is `${number}` { - return isString(value) && !isEmptyStringOrWhitespace(value) && !Number.isNaN(Number(value)); + return isString(value) && !isEmptyStringOrWhitespace(value) && value === value.trim() && !Number.isNaN(Number(value)); } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-restricted-types export function isObject(value: unknown): value is object { return !isNull(value) && (typeof value === 'object' || isFunction(value)); } @@ -625,12 +806,12 @@ export function isObservable(value: unknown): value is ObservableLike { return false; } - // eslint-disable-next-line no-use-extend-native/no-use-extend-native, @typescript-eslint/no-unsafe-call - if (value === (value as any)[Symbol.observable]?.()) { + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access + if (Symbol.observable !== undefined && value === (value as any)[Symbol.observable]?.()) { return true; } - // eslint-disable-next-line @typescript-eslint/no-unsafe-call + // eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access if (value === (value as any)['@@observable']?.()) { return true; } @@ -638,10 +819,15 @@ export function isObservable(value: unknown): value is ObservableLike { return false; } -export function isOddInteger(value: unknown): value is number { +export function isOddInteger(value: Input): value is NumericGuardResult; +export function isOddInteger(value: unknown): boolean { return isAbsoluteModule2(1)(value); } +export function isOneOf(values: T): (value: unknown) => value is T[number] { + return (value: unknown): value is T[number] => values.includes(value); +} + export function isPlainObject(value: unknown): value is Record { // From: https://github.com/sindresorhus/is-plain-obj/blob/main/index.js if (typeof value !== 'object' || value === null) { @@ -654,7 +840,13 @@ export function isPlainObject(value: unknown): value is Record< return (prototype === null || prototype === Object.prototype || Object.getPrototypeOf(prototype) === null) && !(Symbol.toStringTag in value) && !(Symbol.iterator in value); } -export function isPositiveNumber(value: unknown): value is number { +export function isPositiveInteger(value: Input): value is NumericGuardResult; +export function isPositiveInteger(value: unknown): boolean { + return isInteger(value) && value > 0; +} + +export function isPositiveNumber(value: Input): value is NumericGuardResult; +export function isPositiveNumber(value: unknown): boolean { return isNumber(value) && value > 0; } @@ -666,7 +858,7 @@ export function isPromise(value: unknown): value is Promise { return isNativePromise(value) || hasPromiseApi(value); } -// `PropertyKey` is any value that can be used as an object key (string, number, or symbol) +// `PropertyKey` is any value that can be used as an object key (string, number, or symbol). Note: NaN is technically `typeof 'number'` and thus fits TypeScript's `PropertyKey`, but we intentionally exclude it here because using NaN as a property key is almost always a mistake. export function isPropertyKey(value: unknown): value is PropertyKey { return isAny([isString, isNumber, isSymbol], value); } @@ -675,7 +867,8 @@ export function isRegExp(value: unknown): value is RegExp { return getObjectType(value) === 'RegExp'; } -export function isSafeInteger(value: unknown): value is number { +export function isSafeInteger(value: Input): value is NumericGuardResult; +export function isSafeInteger(value: unknown): boolean { return Number.isSafeInteger(value); } @@ -701,10 +894,8 @@ export function isTruthy(value: T | Falsy): value is T { return Boolean(value); } -type TypeGuard = (value: unknown) => value is T; - -// eslint-disable-next-line @typescript-eslint/ban-types -type ResolveTypesOfTypeGuardsTuple = +// eslint-disable-next-line @typescript-eslint/no-restricted-types +type ResolveTypesOfTypeGuardsTuple = TypeGuardsOfT extends [TypeGuard, ...infer TOthers] ? ResolveTypesOfTypeGuardsTuple : TypeGuardsOfT extends undefined[] @@ -752,7 +943,7 @@ export function isUrlSearchParams(value: unknown): value is URLSearchParams { return getObjectType(value) === 'URLSearchParams'; } -export function isUrlString(value: unknown): value is string { +export function isUrlString(value: unknown): value is UrlString { if (!isString(value)) { return false; } @@ -769,35 +960,34 @@ export function isValidDate(value: unknown): value is Date { return isDate(value) && !isNan(Number(value)); } -export function isValidLength(value: unknown): value is number { +export function isValidLength(value: Input): value is NumericGuardResult; +export function isValidLength(value: unknown): boolean { return isSafeInteger(value) && value >= 0; } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-restricted-types export function isWeakMap(value: unknown): value is WeakMap { return getObjectType(value) === 'WeakMap'; } -// eslint-disable-next-line @typescript-eslint/ban-types, unicorn/prevent-abbreviations +// eslint-disable-next-line @typescript-eslint/no-restricted-types export function isWeakRef(value: unknown): value is WeakRef { return getObjectType(value) === 'WeakRef'; } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-restricted-types export function isWeakSet(value: unknown): value is WeakSet { return getObjectType(value) === 'WeakSet'; } export function isWhitespaceString(value: unknown): value is Whitespace { - return isString(value) && /^\s+$/.test(value); + return isString(value) && /^\s+$/v.test(value); } type ArrayMethod = (function_: (value: unknown, index: number, array: unknown[]) => boolean, thisArgument?: unknown) => boolean; function predicateOnArray(method: ArrayMethod, predicate: Predicate, values: unknown[]) { - if (!isFunction(predicate)) { - throw new TypeError(`Invalid predicate: ${JSON.stringify(predicate)}`); - } + validatePredicate(predicate); if (values.length === 0) { throw new TypeError('Invalid number of values'); @@ -810,6 +1000,17 @@ function typeErrorMessage(description: AssertionTypeDescription, value: unknown) return `Expected value which is \`${description}\`, received value of type \`${is(value)}\`.`; } +function typeErrorMessageNot(description: AssertionTypeDescription, value: unknown): string { + return `Expected value which is not \`${description}\`, received value of type \`${is(value)}\`.`; +} + +type NotAssertionResult = Exclude & ([unknown] extends [Value] ? UnknownResult : unknown); + +type NotAssertion = (value: Value, message?: string) => asserts value is NotAssertionResult; + +// eslint-disable-next-line @typescript-eslint/no-restricted-types +type UnknownNotPrimitive = Exclude | object; + function unique(values: T[]): T[] { // eslint-disable-next-line unicorn/prefer-spread return Array.from(new Set(values)); @@ -825,17 +1026,24 @@ function typeErrorMessageMultipleValues(expectedType: AssertionTypeDescription | } // Type assertions have to be declared with an explicit type. +// Keep assertion outputs unbranded even when the corresponding `is.*` guard uses a branded subtype. +// The brands exist to preserve useful false-branch narrowing for type guards on `number` inputs, which does not apply to `asserts`. type Assert = { // Unknowns. undefined: (value: unknown, message?: string) => asserts value is undefined; string: (value: unknown, message?: string) => asserts value is string; number: (value: unknown, message?: string) => asserts value is number; + finiteNumber: (value: unknown, message?: string) => asserts value is number; positiveNumber: (value: unknown, message?: string) => asserts value is number; + negativeInteger: (value: unknown, message?: string) => asserts value is number; negativeNumber: (value: unknown, message?: string) => asserts value is number; + nonNegativeInteger: (value: unknown, message?: string) => asserts value is number; + nonNegativeNumber: (value: unknown, message?: string) => asserts value is number; + positiveInteger: (value: unknown, message?: string) => asserts value is number; bigint: (value: unknown, message?: string) => asserts value is bigint; - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type function: (value: unknown, message?: string) => asserts value is Function; - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-restricted-types null: (value: unknown, message?: string) => asserts value is null; class: (value: unknown, message?: string) => asserts value is Class; boolean: (value: unknown, message?: string) => asserts value is boolean; @@ -844,7 +1052,7 @@ type Assert = { array: (value: unknown, assertion?: (element: unknown) => asserts element is T, message?: string) => asserts value is T[]; buffer: (value: unknown, message?: string) => asserts value is NodeBuffer; blob: (value: unknown, message?: string) => asserts value is Blob; - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-restricted-types nullOrUndefined: (value: unknown, message?: string) => asserts value is null | undefined; object: (value: unknown, message?: string) => asserts value is Record; iterable: (value: unknown, message?: string) => asserts value is Iterable; @@ -855,20 +1063,20 @@ type Assert = { promise: (value: unknown, message?: string) => asserts value is Promise; generatorFunction: (value: unknown, message?: string) => asserts value is GeneratorFunction; asyncGeneratorFunction: (value: unknown, message?: string) => asserts value is AsyncGeneratorFunction; - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type asyncFunction: (value: unknown, message?: string) => asserts value is Function; - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type boundFunction: (value: unknown, message?: string) => asserts value is Function; regExp: (value: unknown, message?: string) => asserts value is RegExp; date: (value: unknown, message?: string) => asserts value is Date; error: (value: unknown, message?: string) => asserts value is Error; map: (value: unknown, message?: string) => asserts value is Map; set: (value: unknown, message?: string) => asserts value is Set; - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-restricted-types weakMap: (value: unknown, message?: string) => asserts value is WeakMap; - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-restricted-types weakSet: (value: unknown, message?: string) => asserts value is WeakSet; - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-restricted-types weakRef: (value: unknown, message?: string) => asserts value is WeakRef; int8Array: (value: unknown, message?: string) => asserts value is Int8Array; uint8Array: (value: unknown, message?: string) => asserts value is Uint8Array; @@ -886,7 +1094,7 @@ type Assert = { dataView: (value: unknown, message?: string) => asserts value is DataView; enumCase: (value: unknown, targetEnum: T, message?: string) => asserts value is T[keyof T]; urlInstance: (value: unknown, message?: string) => asserts value is URL; - urlString: (value: unknown, message?: string) => asserts value is string; + urlString: (value: unknown, message?: string) => asserts value is UrlString; truthy: (value: T | Falsy, message?: string) => asserts value is T; falsy: (value: unknown, message?: string) => asserts value is Falsy; nan: (value: unknown, message?: string) => asserts value is number; @@ -928,14 +1136,73 @@ type Assert = { directInstanceOf: (instance: unknown, class_: Class, message?: string) => asserts instance is T; inRange: (value: number, range: number | [number, number], message?: string) => asserts value is number; + not: NotAssert; + // Variadic functions. - any: (predicate: Predicate | Predicate[], ...values: unknown[]) => void | never; - all: (predicate: Predicate, ...values: unknown[]) => void | never; + any: (predicate: Predicate | readonly Predicate[], ...values: unknown[]) => void | never; + all: (predicate: Predicate | readonly Predicate[], ...values: unknown[]) => void | never; + + /** + Asserts that `value` is `undefined` or satisfies the provided `assertion`. + + Useful for optional inputs. + */ + optional: (value: unknown, assertion: (value: unknown, message?: string) => asserts value is T, message?: string) => asserts value is T | undefined; +}; + +type NotAssert = { + undefined: NotAssertion>; + // eslint-disable-next-line @typescript-eslint/no-restricted-types + null: NotAssertion>; + // eslint-disable-next-line @typescript-eslint/no-restricted-types + nullOrUndefined: NotAssertion>; + string: NotAssertion>; + boolean: NotAssertion>; + symbol: NotAssertion>; + bigint: NotAssertion>; + // eslint-disable-next-line @typescript-eslint/no-restricted-types + primitive: NotAssertion; +}; + +// Negative assertions are limited to types where the assertion rejects every TypeScript value assignable to the forbidden type. Structural object types such as `Map`, `Set`, `Date`, and `Array` are excluded because TypeScript accepts shape-compatible mocks while the runtime checks use object brands, so `Exclude` would narrow values that can pass the negative assertion. +function createAssertNot(predicate: Predicate, description: AssertionTypeDescription): NotAssertion { + return (value: Value, message?: string): asserts value is NotAssertionResult => { + if (predicate(value)) { + throw new TypeError(message ?? typeErrorMessageNot(description, value)); + } + }; +} + +export const assertNotUndefined: NotAssertion> = createAssertNot>(isUndefined, 'undefined'); +// eslint-disable-next-line @typescript-eslint/no-restricted-types +export const assertNotNull: NotAssertion> = createAssertNot>(isNull, 'null'); +// eslint-disable-next-line @typescript-eslint/no-restricted-types +export const assertNotNullOrUndefined: NotAssertion> + // eslint-disable-next-line @typescript-eslint/no-restricted-types + = createAssertNot>(isNullOrUndefined, 'null or undefined'); +export const assertNotString: NotAssertion> = createAssertNot>(isString, 'string'); +export const assertNotBoolean: NotAssertion> = createAssertNot>(isBoolean, 'boolean'); +export const assertNotSymbol: NotAssertion> = createAssertNot>(isSymbol, 'symbol'); +export const assertNotBigint: NotAssertion> = createAssertNot>(isBigint, 'bigint'); +export const assertNotPrimitive: NotAssertion = createAssertNot(isPrimitive, 'primitive'); // eslint-disable-line @typescript-eslint/no-restricted-types + +// We intentionally do not support `assert.not(is.undefined, value)`. TypeScript cannot derive safe complement types from arbitrary predicates, and many predicates here are refinements (for example, `is.number` rejects `NaN`). Explicit methods keep runtime checks and type narrowing aligned. +const notAssertions: NotAssert = { + bigint: assertNotBigint, + boolean: assertNotBoolean, + null: assertNotNull, + nullOrUndefined: assertNotNullOrUndefined, + primitive: assertNotPrimitive, + string: assertNotString, + symbol: assertNotSymbol, + undefined: assertNotUndefined, }; export const assert: Assert = { all: assertAll, any: assertAny, + not: notAssertions, + optional: assertOptional, array: assertArray, arrayBuffer: assertArrayBuffer, arrayLike: assertArrayLike, @@ -964,6 +1231,7 @@ export const assert: Assert = { error: assertError, evenInteger: assertEvenInteger, falsy: assertFalsy, + finiteNumber: assertFiniteNumber, float32Array: assertFloat32Array, float64Array: assertFloat64Array, formData: assertFormData, @@ -981,6 +1249,7 @@ export const assert: Assert = { map: assertMap, nan: assertNan, nativePromise: assertNativePromise, + negativeInteger: assertNegativeInteger, negativeNumber: assertNegativeNumber, nodeStream: assertNodeStream, nonEmptyArray: assertNonEmptyArray, @@ -989,6 +1258,8 @@ export const assert: Assert = { nonEmptySet: assertNonEmptySet, nonEmptyString: assertNonEmptyString, nonEmptyStringAndNotWhitespace: assertNonEmptyStringAndNotWhitespace, + nonNegativeInteger: assertNonNegativeInteger, + nonNegativeNumber: assertNonNegativeNumber, null: assertNull, nullOrUndefined: assertNullOrUndefined, number: assertNumber, @@ -997,6 +1268,7 @@ export const assert: Assert = { observable: assertObservable, oddInteger: assertOddInteger, plainObject: assertPlainObject, + positiveInteger: assertPositiveInteger, positiveNumber: assertPositiveNumber, primitive: assertPrimitive, promise: assertPromise, @@ -1039,7 +1311,7 @@ const methodTypeMap = { isBigUint64Array: 'BigUint64Array', isBlob: 'Blob', isBoolean: 'boolean', - isBoundFunction: 'Function', + isBoundFunction: 'bound Function', isBuffer: 'Buffer', isClass: 'Class', isDataView: 'DataView', @@ -1055,6 +1327,7 @@ const methodTypeMap = { isError: 'Error', isEvenInteger: 'even integer', isFalsy: 'falsy', + isFiniteNumber: 'finite number', isFloat32Array: 'Float32Array', isFloat64Array: 'Float64Array', isFormData: 'FormData', @@ -1072,6 +1345,7 @@ const methodTypeMap = { isMap: 'Map', isNan: 'NaN', isNativePromise: 'native Promise', + isNegativeInteger: 'negative integer', isNegativeNumber: 'negative number', isNodeStream: 'Node.js Stream', isNonEmptyArray: 'non-empty array', @@ -1080,6 +1354,8 @@ const methodTypeMap = { isNonEmptySet: 'non-empty set', isNonEmptyString: 'non-empty string', isNonEmptyStringAndNotWhitespace: 'non-empty string and not whitespace', + isNonNegativeInteger: 'non-negative integer', + isNonNegativeNumber: 'non-negative number', isNull: 'null', isNullOrUndefined: 'null or undefined', isNumber: 'number', @@ -1088,12 +1364,13 @@ const methodTypeMap = { isObservable: 'Observable', isOddInteger: 'odd integer', isPlainObject: 'plain object', + isPositiveInteger: 'positive integer', isPositiveNumber: 'positive number', isPrimitive: 'primitive', isPromise: 'Promise', isPropertyKey: 'PropertyKey', isRegExp: 'RegExp', - isSafeInteger: 'integer', + isSafeInteger: 'safe integer', isSet: 'Set', isSharedArrayBuffer: 'SharedArrayBuffer', isString: 'string', @@ -1117,10 +1394,6 @@ const methodTypeMap = { isWhitespaceString: 'whitespace string', } as const; -function keysOf>(value: T): Array { - return Object.keys(value) as Array; -} - type IsMethodName = keyof typeof methodTypeMap; const isMethodNames: IsMethodName[] = keysOf(methodTypeMap); @@ -1128,21 +1401,36 @@ function isIsMethodName(value: unknown): value is IsMethodName { return isMethodNames.includes(value as IsMethodName); } -export function assertAll(predicate: Predicate, ...values: unknown[]): void | never { +export function assertAll(predicate: Predicate | readonly Predicate[], ...values: unknown[]): void | never { + if (values.length === 0) { + throw new TypeError('Invalid number of values'); + } + if (!isAll(predicate, ...values)) { - const expectedType = isIsMethodName(predicate.name) ? methodTypeMap[predicate.name] : 'predicate returns truthy for all values'; + const predicateFunction = predicate as Predicate; + const expectedType = !Array.isArray(predicate) && isIsMethodName(predicateFunction.name) ? methodTypeMap[predicateFunction.name] : 'predicate returns truthy for all values'; throw new TypeError(typeErrorMessageMultipleValues(expectedType, values)); } } -export function assertAny(predicate: Predicate | Predicate[], ...values: unknown[]): void | never { +export function assertAny(predicate: Predicate | readonly Predicate[], ...values: unknown[]): void | never { + if (values.length === 0) { + throw new TypeError('Invalid number of values'); + } + if (!isAny(predicate, ...values)) { - const predicates = isArray(predicate) ? predicate : [predicate]; - const expectedTypes = predicates.map(predicate => isIsMethodName(predicate.name) ? methodTypeMap[predicate.name] : 'predicate returns truthy for any value'); + const predicates = Array.isArray(predicate) ? predicate as readonly Predicate[] : [predicate as Predicate]; + const expectedTypes = predicates.map(singlePredicate => isIsMethodName(singlePredicate.name) ? methodTypeMap[singlePredicate.name] : 'predicate returns truthy for any value'); throw new TypeError(typeErrorMessageMultipleValues(expectedTypes, values)); } } +export function assertOptional(value: unknown, assertion: (value: unknown, message?: string) => asserts value is T, message?: string): asserts value is T | undefined { + if (!isUndefined(value)) { + assertion(value, message); + } +} + export function assertArray(value: unknown, assertion?: (element: unknown, message?: string) => asserts element is T, message?: string): asserts value is T[] { if (!isArray(value)) { throw new TypeError(message ?? typeErrorMessage('Array', value)); @@ -1168,7 +1456,7 @@ export function assertArrayLike(value: unknown, message?: string): } } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function assertAsyncFunction(value: unknown, message?: string): asserts value is Function { if (!isAsyncFunction(value)) { throw new TypeError(message ?? typeErrorMessage('AsyncFunction', value)); @@ -1223,10 +1511,10 @@ export function assertBoolean(value: unknown, message?: string): asserts value i } } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function assertBoundFunction(value: unknown, message?: string): asserts value is Function { if (!isBoundFunction(value)) { - throw new TypeError(message ?? typeErrorMessage('Function', value)); + throw new TypeError(message ?? typeErrorMessage('bound Function', value)); } } @@ -1323,6 +1611,12 @@ export function assertFalsy(value: unknown, message?: string): asserts value is } } +export function assertFiniteNumber(value: unknown, message?: string): asserts value is number { + if (!isFiniteNumber(value)) { + throw new TypeError(message ?? typeErrorMessage('finite number', value)); + } +} + export function assertFloat32Array(value: unknown, message?: string): asserts value is Float32Array { if (!isFloat32Array(value)) { throw new TypeError(message ?? typeErrorMessage('Float32Array', value)); @@ -1341,7 +1635,7 @@ export function assertFormData(value: unknown, message?: string): asserts value } } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-unsafe-function-type export function assertFunction(value: unknown, message?: string): asserts value is Function { if (!isFunction(value)) { throw new TypeError(message ?? typeErrorMessage('Function', value)); @@ -1426,6 +1720,12 @@ export function assertNativePromise(value: unknown, message?: strin } } +export function assertNegativeInteger(value: unknown, message?: string): asserts value is number { + if (!isNegativeInteger(value)) { + throw new TypeError(message ?? typeErrorMessage('negative integer', value)); + } +} + export function assertNegativeNumber(value: unknown, message?: string): asserts value is number { if (!isNegativeNumber(value)) { throw new TypeError(message ?? typeErrorMessage('negative number', value)); @@ -1474,14 +1774,26 @@ export function assertNonEmptyStringAndNotWhitespace(value: unknown, message?: s } } -// eslint-disable-next-line @typescript-eslint/ban-types +export function assertNonNegativeInteger(value: unknown, message?: string): asserts value is number { + if (!isNonNegativeInteger(value)) { + throw new TypeError(message ?? typeErrorMessage('non-negative integer', value)); + } +} + +export function assertNonNegativeNumber(value: unknown, message?: string): asserts value is number { + if (!isNonNegativeNumber(value)) { + throw new TypeError(message ?? typeErrorMessage('non-negative number', value)); + } +} + +// eslint-disable-next-line @typescript-eslint/no-restricted-types export function assertNull(value: unknown, message?: string): asserts value is null { if (!isNull(value)) { throw new TypeError(message ?? typeErrorMessage('null', value)); } } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-restricted-types export function assertNullOrUndefined(value: unknown, message?: string): asserts value is null | undefined { if (!isNullOrUndefined(value)) { throw new TypeError(message ?? typeErrorMessage('null or undefined', value)); @@ -1500,7 +1812,7 @@ export function assertNumericString(value: unknown, message?: string): asserts v } } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-restricted-types export function assertObject(value: unknown, message?: string): asserts value is object { if (!isObject(value)) { throw new TypeError(message ?? typeErrorMessage('Object', value)); @@ -1525,6 +1837,12 @@ export function assertPlainObject(value: unknown, message?: str } } +export function assertPositiveInteger(value: unknown, message?: string): asserts value is number { + if (!isPositiveInteger(value)) { + throw new TypeError(message ?? typeErrorMessage('positive integer', value)); + } +} + export function assertPositiveNumber(value: unknown, message?: string): asserts value is number { if (!isPositiveNumber(value)) { throw new TypeError(message ?? typeErrorMessage('positive number', value)); @@ -1543,7 +1861,7 @@ export function assertPromise(value: unknown, message?: string): as } } -export function assertPropertyKey(value: unknown, message?: string): asserts value is number { +export function assertPropertyKey(value: unknown, message?: string): asserts value is PropertyKey { if (!isPropertyKey(value)) { throw new TypeError(message ?? typeErrorMessage('PropertyKey', value)); } @@ -1557,7 +1875,7 @@ export function assertRegExp(value: unknown, message?: string): asserts value is export function assertSafeInteger(value: unknown, message?: string): asserts value is number { if (!isSafeInteger(value)) { - throw new TypeError(message ?? typeErrorMessage('integer', value)); + throw new TypeError(message ?? typeErrorMessage('safe integer', value)); } } @@ -1646,7 +1964,7 @@ export function assertUrlSearchParams(value: unknown, message?: string): asserts } } -export function assertUrlString(value: unknown, message?: string): asserts value is string { +export function assertUrlString(value: unknown, message?: string): asserts value is UrlString { if (!isUrlString(value)) { throw new TypeError(message ?? typeErrorMessage('string with a URL', value)); } @@ -1664,21 +1982,21 @@ export function assertValidLength(value: unknown, message?: string): asserts val } } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-restricted-types export function assertWeakMap(value: unknown, message?: string): asserts value is WeakMap { if (!isWeakMap(value)) { throw new TypeError(message ?? typeErrorMessage('WeakMap', value)); } } -// eslint-disable-next-line @typescript-eslint/ban-types, unicorn/prevent-abbreviations +// eslint-disable-next-line @typescript-eslint/no-restricted-types export function assertWeakRef(value: unknown, message?: string): asserts value is WeakRef { if (!isWeakRef(value)) { throw new TypeError(message ?? typeErrorMessage('WeakRef', value)); } } -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-restricted-types export function assertWeakSet(value: unknown, message?: string): asserts value is WeakSet { if (!isWeakSet(value)) { throw new TypeError(message ?? typeErrorMessage('WeakSet', value)); @@ -1696,9 +2014,25 @@ export default is; export type { ArrayLike, Class, + EvenInteger, + FiniteNumber, + Integer, + NaN, + NegativeInfinity, + NegativeInteger, + NegativeNumber, NodeStream, + NonNegativeInteger, + NonNegativeNumber, ObservableLike, + OddInteger, + PositiveInfinity, + PositiveInteger, + PositiveNumber, Predicate, Primitive, + SafeInteger, TypedArray, -} from './types.js'; + UrlString, + ValidLength, +} from './types.ts'; diff --git a/source/types.ts b/source/types.ts index b79a603..9255072 100644 --- a/source/types.ts +++ b/source/types.ts @@ -4,7 +4,8 @@ Matches any [primitive value](https://developer.mozilla.org/en-US/docs/Glossary/Primitive). */ export type Primitive = - | null // eslint-disable-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-restricted-types + | null | undefined | string | number @@ -53,10 +54,11 @@ export type ObservableLike = { [Symbol.observable](): ObservableLike; }; -// eslint-disable-next-line @typescript-eslint/ban-types +// eslint-disable-next-line @typescript-eslint/no-restricted-types export type Falsy = false | 0 | 0n | '' | null | undefined; -export type WeakRef = { // eslint-disable-line @typescript-eslint/ban-types, unicorn/prevent-abbreviations +// eslint-disable-next-line @typescript-eslint/no-restricted-types +export type WeakRef = { readonly [Symbol.toStringTag]: 'WeakRef'; deref(): T | undefined; }; @@ -75,3 +77,123 @@ export type Predicate = (value: unknown) => boolean; export type NonEmptyString = string & {0: string}; export type Whitespace = ' '; + +type Brand = Readonly>; + +/** +A string that represents a valid URL. + +This is a branded type to prevent incorrect TypeScript type narrowing. +*/ +export type UrlString = string & {readonly __brand: 'UrlString'}; + +// Keep numeric guards branded and simple. This intentionally favors correct false-branch narrowing for `number` inputs over perfect success-branch narrowing for numeric literal unions. + +/** +The IEEE 754 "Not-a-Number" value, typed as a subtype of `number`. + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type NaN = number & Brand<'__nanBrand'>; + +/** +A finite number (excludes `NaN`, `Infinity`, and `-Infinity`). + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type FiniteNumber = number & Brand<'__finiteNumberBrand'>; + +/** +A number greater than or equal to zero. + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type NonNegativeNumber = number & Brand<'__nonNegativeNumberBrand'>; + +/** +An integer value (no fractional part). + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type Integer = FiniteNumber & Brand<'__integerBrand'>; + +/** +A number greater than zero. + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type PositiveNumber = NonNegativeNumber & Brand<'__positiveNumberBrand'>; + +/** +A number less than zero. + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type NegativeNumber = number & Brand<'__negativeNumberBrand'>; + +/** +An integer less than zero. + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type NegativeInteger = Integer & NegativeNumber & Brand<'__negativeIntegerBrand'>; + +/** +An integer greater than or equal to zero. + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type NonNegativeInteger = Integer & NonNegativeNumber & Brand<'__nonNegativeIntegerBrand'>; + +/** +An integer greater than zero. + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type PositiveInteger = NonNegativeInteger & PositiveNumber & Brand<'__positiveIntegerBrand'>; + +// Note: type-fest uses the `1e999` overflow trick to represent these types (since TypeScript has +// no built-in Infinity type), but we use branded types here for consistency and to avoid +// relying on numeric overflow behavior. + +/** +A positive infinite number (`Infinity`). + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type PositiveInfinity = PositiveNumber & Brand<'__positiveInfinityBrand'>; + +/** +A negative infinite number (`-Infinity`). + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type NegativeInfinity = NegativeNumber & Brand<'__negativeInfinityBrand'>; + +/** +A safe integer (within the range of `Number.MIN_SAFE_INTEGER` to `Number.MAX_SAFE_INTEGER`). + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type SafeInteger = Integer & Brand<'__safeIntegerBrand'>; + +/** +An even integer. + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type EvenInteger = Integer & Brand<'__evenIntegerBrand'>; + +/** +An odd integer. + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type OddInteger = Integer & Brand<'__oddIntegerBrand'>; + +/** +A non-negative safe integer, suitable as an array or string length. + +Branded to prevent false-branch narrowing to `never` when the input is `number`. +*/ +export type ValidLength = SafeInteger & NonNegativeInteger & Brand<'__validLengthBrand'>; diff --git a/source/utilities.ts b/source/utilities.ts new file mode 100644 index 0000000..686edb1 --- /dev/null +++ b/source/utilities.ts @@ -0,0 +1,3 @@ +export function keysOf>(value: T): Array { + return Object.keys(value) as Array; // eslint-disable-line @typescript-eslint/no-unnecessary-type-assertion +} diff --git a/test/test.ts b/test/test.ts index 258fb89..2f98bfb 100644 --- a/test/test.ts +++ b/test/test.ts @@ -1,22 +1,28 @@ -/* eslint-disable @typescript-eslint/no-empty-function */ +/* eslint-disable @typescript-eslint/no-empty-function, @stylistic/curly-newline, @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access, @typescript-eslint/prefer-nullish-coalescing, @typescript-eslint/no-unsafe-argument */ import {Buffer} from 'node:buffer'; import fs from 'node:fs'; import net from 'node:net'; import Stream from 'node:stream'; import {inspect} from 'node:util'; -import test, {type ExecutionContext} from 'ava'; +import {test} from 'node:test'; +import assert from 'node:assert/strict'; import {JSDOM} from 'jsdom'; import {Subject, Observable} from 'rxjs'; import {temporaryFile} from 'tempy'; import {expectTypeOf} from 'expect-type'; import ZenObservable from 'zen-observable'; import is, { - assert, + assert as isAssert, + assertPropertyKey, type AssertionTypeDescription, + type NaN as NaNType, + type Predicate, type Primitive, type TypedArray, type TypeName, -} from '../source/index.js'; + type UrlString, +} from '../source/index.ts'; +import {keysOf} from '../source/utilities.ts'; class PromiseSubclassFixture extends Promise {} class ErrorSubclassFixture extends Error {} @@ -24,500 +30,388 @@ class ErrorSubclassFixture extends Error {} const {window} = new JSDOM(); const {document} = window; -const structuredClone = globalThis.structuredClone ?? (x => x); - -type Test = { - assert: (...arguments_: any[]) => void | never; +type Test = Readonly<{ fixtures: unknown[]; typename?: TypeName; typeDescription?: AssertionTypeDescription; - is(value: unknown): boolean; -}; +}>; -const invertAssertThrow = (description: AssertionTypeDescription, function_: () => void | never, value: unknown): void | never => { - const expectedAssertErrorMessage = `Expected value which is \`${description}\`, received value of type \`${is(value)}\`.`; +// Every entry should be unique and belongs in the most specific type for that entry +const reusableFixtures = { + asyncFunction: [async function () {}, async () => {}], + asyncGeneratorFunction: [ + async function * () {}, + async function * () { + yield 4; + }, + ], + boundFunction: [() => {}, function () {}.bind(null)], // eslint-disable-line no-extra-bind + buffer: [Buffer.from('🦄')], + emptyArray: [[], new Array()], // eslint-disable-line @typescript-eslint/no-array-constructor + emptyMap: [new Map()], + emptySet: [new Set()], + emptyString: ['', String()], + function: [ + function foo() {}, // eslint-disable-line func-names + function () {}, + ], + generatorFunction: [ + function * () {}, + function * () { + yield 4; + }, + ], + infinite: [Number.POSITIVE_INFINITY, Number.NEGATIVE_INFINITY], + integer: [0, -0, 6], + nativePromise: [Promise.resolve(), PromiseSubclassFixture.resolve()], + number: [1.4], + numericString: ['5', '-3.2', 'Infinity', '0x56'], + plainObject: [ + {x: 1}, + Object.create(null), + new Object(), // eslint-disable-line no-object-constructor + structuredClone({x: 1}), + structuredClone(Object.create(null)), + structuredClone(new Object()), // eslint-disable-line no-object-constructor + ], + promise: [Object.create({then() {}, catch() {}})], // eslint-disable-line unicorn/no-thenable + safeInteger: [(2 ** 53) - 1, -(2 ** 53) + 1], +} as const satisfies Partial<{[K in keyof typeof is]: unknown[]}>; - try { - function_(); - } catch (error: unknown) { - 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, +const primitiveTypes = { + undefined: { fixtures: [ undefined, ], typename: 'undefined', - }], - ['null', { - is: is.null, - assert: assert.null, + }, + null: { fixtures: [ null, ], typename: 'null', - }], - ['string', { - is: is.string, - assert: assert.string, + }, + string: { fixtures: [ '🦄', 'hello world', - '', + ...reusableFixtures.emptyString, + ...reusableFixtures.numericString, ], typename: 'string', - }], - ['emptyString', { - is: is.emptyString, - assert: assert.emptyString, - fixtures: [ - '', - String(), - ], + }, + emptyString: { + fixtures: [...reusableFixtures.emptyString], typename: 'string', typeDescription: 'empty string', - }], - ['number', { - is: is.number, - assert: assert.number, + }, + number: { fixtures: [ - 6, - 1.4, - 0, - -0, - Number.POSITIVE_INFINITY, - Number.NEGATIVE_INFINITY, + ...reusableFixtures.number, + ...reusableFixtures.infinite, + ...reusableFixtures.integer, + ...reusableFixtures.safeInteger, ], typename: 'number', - }], - ['bigint', { - is: is.bigint, - assert: assert.bigint, + }, + bigint: { fixtures: [ - // Disabled until TS supports it for an ESnnnn target. - // 1n, - // 0n, - // -0n, - BigInt('1234'), + 1n, + 0n, + -0n, + 1234n, ], typename: 'bigint', - }], - ['boolean', { - is: is.boolean, - assert: assert.boolean, + }, + boolean: { fixtures: [ true, false, ], typename: 'boolean', - }], - ['symbol', { - is: is.symbol, - assert: assert.symbol, - fixtures: [ - Symbol('🦄'), - ], - typename: 'symbol', - }], - ['numericString', { - is: is.numericString, - assert: assert.numericString, - fixtures: [ - '5', - '-3.2', - 'Infinity', - '0x56', - ], + }, + numericString: { + fixtures: [...reusableFixtures.numericString], typename: 'string', typeDescription: 'string with a number', - }], - ['array', { - is: is.array, - assert: assert.array, - fixtures: [ - [1, 2], - Array.from({length: 2}), - ], - typename: 'Array', - }], - ['emptyArray', { - is: is.emptyArray, - assert: assert.emptyArray, - fixtures: [ - [], - new Array(), // eslint-disable-line @typescript-eslint/no-array-constructor - ], - typename: 'Array', - typeDescription: 'empty array', - }], - ['function', { - is: is.function, - assert: assert.function, - fixtures: [ - function foo() {}, // eslint-disable-line func-names - function () {}, - () => {}, - async function () {}, - function * (): unknown {}, - async function * (): unknown {}, - ], - typename: 'Function', - }], - ['buffer', { - is: is.buffer, - assert: assert.buffer, - fixtures: [ - Buffer.from('🦄'), - ], - typename: 'Buffer', - }], - ['blob', { - is: is.blob, - assert: assert.blob, - fixtures: [ - new window.Blob(), - ], - typename: 'Blob', - }], - ['object', { - is: is.object, - assert: assert.object, - fixtures: [ - {x: 1}, - Object.create({x: 1}), - ], - typename: 'Object', - }], - ['regExp', { - is: is.regExp, - assert: assert.regExp, - fixtures: [ - /\w/, - new RegExp('\\w'), // eslint-disable-line prefer-regex-literals - ], - typename: 'RegExp', - }], - ['date', { - is: is.date, - assert: assert.date, - fixtures: [ - new Date(), - ], - typename: 'Date', - }], - ['error', { - is: is.error, - assert: assert.error, - fixtures: [ - new Error('🦄'), - new ErrorSubclassFixture(), - ], - typename: 'Error', - }], - ['nativePromise', { - is: is.nativePromise, - assert: assert.nativePromise, - fixtures: [ - Promise.resolve(), - PromiseSubclassFixture.resolve(), - ], - typename: 'Promise', - typeDescription: 'native Promise', - }], - ['promise', { - is: is.promise, - assert: assert.promise, - fixtures: [ - {then() {}, catch() {}}, // eslint-disable-line unicorn/no-thenable - ], - typename: 'Object', - typeDescription: 'Promise', - }], - ['generator', { - is: is.generator, - assert: assert.generator, - fixtures: [ - (function * () { - yield 4; - })(), - ], - typename: 'Generator', - }], - ['asyncGenerator', { - is: is.asyncGenerator, - assert: assert.asyncGenerator, - fixtures: [ - (async function * () { - yield 4; - })(), - ], - typename: 'AsyncGenerator', - }], - ['generatorFunction', { - is: is.generatorFunction, - assert: assert.generatorFunction, - fixtures: [ - function * () { - yield 4; - }, - ], - typename: 'Function', - typeDescription: 'GeneratorFunction', - }], - ['asyncGeneratorFunction', { - is: is.asyncGeneratorFunction, - assert: assert.asyncGeneratorFunction, - fixtures: [ - async function * () { - yield 4; - }, - ], - typename: 'Function', - typeDescription: 'AsyncGeneratorFunction', - }], - ['asyncFunction', { - is: is.asyncFunction, - assert: assert.asyncFunction, - fixtures: [ - async function () {}, - async () => {}, - ], - typename: 'Function', - typeDescription: 'AsyncFunction', - }], - ['boundFunction', { - is: is.boundFunction, - assert: assert.boundFunction, - fixtures: [ - () => {}, - function () {}.bind(null), // eslint-disable-line no-extra-bind - ], - typename: 'Function', - }], - ['map', { - is: is.map, - assert: assert.map, - fixtures: [ - new Map([['one', '1']]), - ], - typename: 'Map', - }], - ['emptyMap', { - is: is.emptyMap, - assert: assert.emptyMap, - fixtures: [ - new Map(), - ], - typename: 'Map', - typeDescription: 'empty map', - }], - ['set', { - is: is.set, - assert: assert.set, - fixtures: [ - new Set(['one']), - ], - typename: 'Set', - }], - ['emptySet', { - is: is.emptySet, - assert: assert.emptySet, - fixtures: [ - new Set(), - ], - typename: 'Set', - typeDescription: 'empty set', - }], - ['weakSet', { - is: is.weakSet, - assert: assert.weakSet, - fixtures: [ - new WeakSet(), - ], - typename: 'WeakSet', - }], - ['weakRef', { - is: is.weakRef, - assert: assert.weakRef, - fixtures: window.WeakRef ? [new window.WeakRef({})] : [], - typename: 'WeakRef', - }], - ['weakMap', { - is: is.weakMap, - assert: assert.weakMap, - fixtures: [ - new WeakMap(), - ], - typename: 'WeakMap', - }], - ['int8Array', { - is: is.int8Array, - assert: assert.int8Array, - fixtures: [ - new Int8Array(), - ], - typename: 'Int8Array', - }], - ['uint8Array', { - is: is.uint8Array, - assert: assert.uint8Array, - fixtures: [ - new Uint8Array(), - ], - typename: 'Uint8Array', - }], - ['uint8ClampedArray', { - is: is.uint8ClampedArray, - assert: assert.uint8ClampedArray, - fixtures: [ - new Uint8ClampedArray(), - ], - typename: 'Uint8ClampedArray', - }], - ['int16Array', { - is: is.int16Array, - assert: assert.int16Array, - fixtures: [ - new Int16Array(), - ], - typename: 'Int16Array', - }], - ['uint16Array', { - is: is.uint16Array, - assert: assert.uint16Array, - fixtures: [ - new Uint16Array(), - ], - typename: 'Uint16Array', - }], - ['int32Array', { - is: is.int32Array, - assert: assert.int32Array, - fixtures: [ - new Int32Array(), - ], - typename: 'Int32Array', - }], - ['uint32Array', { - is: is.uint32Array, - assert: assert.uint32Array, - fixtures: [ - new Uint32Array(), - ], - typename: 'Uint32Array', - }], - ['float32Array', { - is: is.float32Array, - assert: assert.float32Array, - fixtures: [ - new Float32Array(), - ], - typename: 'Float32Array', - }], - ['float64Array', { - is: is.float64Array, - assert: assert.float64Array, - fixtures: [ - new Float64Array(), - ], - typename: 'Float64Array', - }], - ['bigInt64Array', { - is: is.bigInt64Array, - assert: assert.bigInt64Array, - fixtures: [ - new BigInt64Array(), - ], - typename: 'BigInt64Array', - }], - ['bigUint64Array', { - is: is.bigUint64Array, - assert: assert.bigUint64Array, - fixtures: [ - new BigUint64Array(), - ], - typename: 'BigUint64Array', - }], - ['arrayBuffer', { - is: is.arrayBuffer, - assert: assert.arrayBuffer, - fixtures: [ - new ArrayBuffer(10), - ], - typename: 'ArrayBuffer', - }], - ['dataView', { - is: is.dataView, - assert: assert.dataView, - fixtures: [ - new DataView(new ArrayBuffer(10)), - ], - typename: 'DataView', - }], - ['nan', { - is: is.nan, - assert: assert.nan, + }, + nan: { fixtures: [ NaN, // eslint-disable-line unicorn/prefer-number-properties Number.NaN, ], typename: 'NaN', typeDescription: 'NaN', - }], - ['nullOrUndefined', { - is: is.nullOrUndefined, - assert: assert.nullOrUndefined, + }, + nullOrUndefined: { fixtures: [ null, undefined, ], typeDescription: 'null or undefined', - }], - ['plainObject', { - is: is.plainObject, - assert: assert.plainObject, + }, + integer: { + fixtures: [...reusableFixtures.integer, ...reusableFixtures.safeInteger], + typename: 'number', + typeDescription: 'integer', + }, + safeInteger: { + fixtures: [...reusableFixtures.integer, ...reusableFixtures.safeInteger], + typename: 'number', + typeDescription: 'safe integer', + }, + infinite: { + fixtures: [...reusableFixtures.infinite], + typename: 'number', + typeDescription: 'infinite number', + }, +} as const satisfies Partial<{[K in keyof typeof is]: Test}>; + +const objectTypes = { + symbol: { fixtures: [ - {x: 1}, - Object.create(null), - new Object(), // eslint-disable-line no-object-constructor - structuredClone({x: 1}), - structuredClone(Object.create(null)), - structuredClone(new Object()), // eslint-disable-line no-object-constructor + Symbol('🦄'), + ], + typename: 'symbol', + }, + array: { + fixtures: [ + [1, 2], + Array.from({length: 2}), + ...reusableFixtures.emptyArray, + ], + typename: 'Array', + }, + emptyArray: { + fixtures: [...reusableFixtures.emptyArray], + typename: 'Array', + typeDescription: 'empty array', + }, + function: { + fixtures: [ + ...reusableFixtures.asyncFunction, + ...reusableFixtures.asyncGeneratorFunction, + ...reusableFixtures.boundFunction, + ...reusableFixtures.function, + ...reusableFixtures.generatorFunction, + ], + typename: 'Function', + }, + buffer: { + fixtures: [...reusableFixtures.buffer], + typename: 'Buffer', + }, + blob: { + fixtures: [ + new window.Blob(), + ], + typename: 'Blob', + }, + object: { + fixtures: [ + Object.create({x: 1}), + {[Symbol.toStringTag]: 'String'}, + ...reusableFixtures.plainObject, + ], + typename: 'Object', + }, + regExp: { + fixtures: [ + /\w/v, + // eslint-disable-next-line prefer-regex-literals + new RegExp(String.raw`\w`, 'v'), + ], + typename: 'RegExp', + }, + date: { + fixtures: [ + new Date(), + ], + typename: 'Date', + }, + error: { + fixtures: [ + new Error('🦄'), + new ErrorSubclassFixture(), + ], + typename: 'Error', + }, + nativePromise: { + fixtures: [...reusableFixtures.nativePromise], + typename: 'Promise', + typeDescription: 'native Promise', + }, + promise: { + fixtures: [ + ...reusableFixtures.nativePromise, + ...reusableFixtures.promise, + ], + typename: 'Promise', + typeDescription: 'Promise', + }, + generator: { + fixtures: [ + (function * () { + yield 4; + })(), + ], + typename: 'Generator', + }, + asyncGenerator: { + fixtures: [ + (async function * () { + yield 4; + })(), + ], + typename: 'AsyncGenerator', + }, + generatorFunction: { + fixtures: [...reusableFixtures.generatorFunction], + typename: 'Function', + typeDescription: 'GeneratorFunction', + }, + asyncGeneratorFunction: { + fixtures: [...reusableFixtures.asyncGeneratorFunction], + typename: 'Function', + typeDescription: 'AsyncGeneratorFunction', + }, + asyncFunction: { + fixtures: [...reusableFixtures.asyncFunction], + typename: 'Function', + typeDescription: 'AsyncFunction', + }, + boundFunction: { + fixtures: [...reusableFixtures.boundFunction, ...reusableFixtures.asyncFunction], + typename: 'Function', + typeDescription: 'bound Function', + }, + map: { + fixtures: [ + new Map([['one', '1']]), + ...reusableFixtures.emptyMap, + ], + typename: 'Map', + }, + emptyMap: { + fixtures: [...reusableFixtures.emptyMap], + typename: 'Map', + typeDescription: 'empty map', + }, + set: { + fixtures: [ + new Set(['one']), + ...reusableFixtures.emptySet, + ], + typename: 'Set', + }, + emptySet: { + fixtures: [...reusableFixtures.emptySet], + typename: 'Set', + typeDescription: 'empty set', + }, + weakSet: { + fixtures: [ + new WeakSet(), + ], + typename: 'WeakSet', + }, + weakRef: { + fixtures: [ + new window.WeakRef({}), + ], + typename: 'WeakRef', + }, + weakMap: { + fixtures: [ + new WeakMap(), + ], + typename: 'WeakMap', + }, + int8Array: { + fixtures: [ + new Int8Array(), + ], + typename: 'Int8Array', + }, + uint8Array: { + fixtures: [ + new Uint8Array(), + ], + typename: 'Uint8Array', + }, + uint8ClampedArray: { + fixtures: [ + new Uint8ClampedArray(), + ], + typename: 'Uint8ClampedArray', + }, + int16Array: { + fixtures: [ + new Int16Array(), + ], + typename: 'Int16Array', + }, + uint16Array: { + fixtures: [ + new Uint16Array(), + ], + typename: 'Uint16Array', + }, + int32Array: { + fixtures: [ + new Int32Array(), + ], + typename: 'Int32Array', + }, + uint32Array: { + fixtures: [ + new Uint32Array(), + ], + typename: 'Uint32Array', + }, + float32Array: { + fixtures: [ + new Float32Array(), + ], + typename: 'Float32Array', + }, + float64Array: { + fixtures: [ + new Float64Array(), + ], + typename: 'Float64Array', + }, + bigInt64Array: { + fixtures: [ + new BigInt64Array(), + ], + typename: 'BigInt64Array', + }, + bigUint64Array: { + fixtures: [ + new BigUint64Array(), + ], + typename: 'BigUint64Array', + }, + arrayBuffer: { + fixtures: [ + new ArrayBuffer(10), + ], + typename: 'ArrayBuffer', + }, + dataView: { + fixtures: [ + new DataView(new ArrayBuffer(10)), + ], + typename: 'DataView', + }, + plainObject: { + fixtures: [ + ...reusableFixtures.plainObject, ], typename: 'Object', typeDescription: 'plain object', - }], - ['integer', { - is: is.integer, - assert: assert.integer, - fixtures: [ - 6, - ], - typename: 'number', - typeDescription: 'integer', - }], - ['safeInteger', { - is: is.safeInteger, - assert: assert.safeInteger, - fixtures: [ - (2 ** 53) - 1, - -(2 ** 53) + 1, - ], - typename: 'number', - typeDescription: 'integer', - }], - ['htmlElement', { - is: is.htmlElement, - assert: assert.htmlElement, + }, + htmlElement: { fixtures: [ 'div', 'input', @@ -528,36 +422,16 @@ const types = new Map([ ] .map(fixture => document.createElement(fixture)), typeDescription: 'HTMLElement', - }], - ['non-htmlElement', { - is: value => !is.htmlElement(value), - assert(value: unknown) { - invertAssertThrow('HTMLElement', () => { - assert.htmlElement(value); - }, value); - }, - fixtures: [ - document.createTextNode('data'), - document.createProcessingInstruction('xml-stylesheet', 'href="mycss.css" type="text/css"'), - document.createComment('This is a comment'), - document, - document.implementation.createDocumentType('svg:svg', '-//W3C//DTD SVG 1.1//EN', 'https://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'), - document.createDocumentFragment(), - ], - }], - ['observable', { - is: is.observable, - assert: assert.observable, + }, + observable: { fixtures: [ new Observable(), new Subject(), new ZenObservable(() => {}), ], typename: 'Observable', - }], - ['nodeStream', { - is: is.nodeStream, - assert: assert.nodeStream, + }, + nodeStream: { fixtures: [ fs.createReadStream('readme.md'), fs.createWriteStream(temporaryFile()), @@ -571,673 +445,820 @@ const types = new Map([ ], typename: 'Object', typeDescription: 'Node.js Stream', - }], - ['infinite', { - is: is.infinite, - assert: assert.infinite, + }, + formData: { fixtures: [ - Number.POSITIVE_INFINITY, - Number.NEGATIVE_INFINITY, + new window.FormData(), ], - typename: 'number', - typeDescription: 'infinite number', - }], + typename: 'FormData', + }, +} as const satisfies Partial<{[K in keyof typeof is]: Test}>; + +const types = { + ...objectTypes, + ...primitiveTypes, +} as const satisfies Partial<{[K in keyof typeof is]: Test}>; + +type TypeNameWithFixture = keyof typeof types; + +const subClasses = new Map([ + ['uint8Array', ['buffer']], // It's too hard to differentiate the two + ['object', keysOf(objectTypes)], ]); +const notAssertionFixtures = { + bigint: {fixture: 1n, nonFixture: '🦄', typeDescription: 'bigint'}, + boolean: {fixture: false, nonFixture: '🦄', typeDescription: 'boolean'}, + null: {fixture: null, nonFixture: '🦄', typeDescription: 'null'}, + nullOrUndefined: {fixtures: [null, undefined], nonFixture: '🦄', typeDescription: 'null or undefined'}, + primitive: {fixtures: [false, null, undefined], nonFixture: [], typeDescription: 'primitive'}, + string: {fixture: '🦄', nonFixture: 1, typeDescription: 'string'}, + symbol: {fixture: Symbol('🦄'), nonFixture: '🦄', typeDescription: 'symbol'}, + undefined: {fixture: undefined, nonFixture: null, typeDescription: 'undefined'}, +} as const satisfies Record; + // This ensures a certain method matches only the types it's supposed to and none of the other methods' types -const testType = (t: ExecutionContext, type: string, exclude?: string[]) => { - const testData = types.get(type); +for (const type of keysOf(types)) { + test(`is.${type}`, () => { + const {fixtures, typeDescription, typename} = types[type] as Test; + const valueType = typeDescription ?? typename ?? 'unspecified'; - if (testData === undefined) { - t.fail(`is.${type} not defined`); - - return; - } - - 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. - // Could reduce the use of `exclude`. - if (exclude?.includes(key)) { - continue; - } - - const isTypeUnderTest = key === type; - const assertIs = isTypeUnderTest ? t.true : t.false; + const testAssert: (value: unknown) => never | void = isAssert[type]; + const testIs: Predicate = is[type]; for (const fixture of fixtures) { - assertIs(testIs(fixture), `Value: ${inspect(fixture)}`); - const valueType = typeDescription ?? typename ?? 'unspecified'; + assert.ok(testIs(fixture), `Value: ${inspect(fixture)}`); + assert.doesNotThrow(() => { + testAssert(fixture); + }); - if (isTypeUnderTest) { - t.notThrows(() => { - testAssert(fixture); - }); - } else { - t.throws(() => { + if (typename !== undefined) { + assert.strictEqual(is(fixture), typename); + } + } + + for (const key of keysOf(types).filter(key => key !== type)) { + if (subClasses.has(type) && subClasses.get(type)?.includes(key)) { + continue; + } + + for (let i = 0; i < types[key].fixtures.length; i += 1) { + const fixture: unknown = types[key].fixtures[i]; + + if (fixtures.includes(fixture)) { + continue; + } + + assert.strictEqual(testIs(fixture), false, `${key}.fixture[${i}]: ${inspect(fixture)} should not be ${type}`); + assert.throws(() => { testAssert(fixture); }, { message: `Expected value which is \`${valueType}\`, received value of type \`${is(fixture)}\`.`, }); } - - if (isTypeUnderTest && typename) { - t.is(is(fixture), typename); - } } - } -}; - -test('is.undefined', t => { - testType(t, 'undefined', ['nullOrUndefined']); -}); - -test('is.null', t => { - testType(t, 'null', ['nullOrUndefined']); -}); - -test('is.string', t => { - testType(t, 'string', ['emptyString', 'numericString']); -}); - -test('is.number', t => { - testType(t, 'number', ['integer', 'safeInteger', 'infinite']); -}); - -test('is.positiveNumber', t => { - t.true(is.positiveNumber(6)); - t.true(is.positiveNumber(1.4)); - t.true(is.positiveNumber(Number.POSITIVE_INFINITY)); - - t.notThrows(() => { - assert.positiveNumber(6); }); - t.notThrows(() => { - assert.positiveNumber(1.4); +} + +test('is.positiveNumber', () => { + assert.ok(is.positiveNumber(6)); + assert.ok(is.positiveNumber(1.4)); + assert.ok(is.positiveNumber(Number.POSITIVE_INFINITY)); + + assert.doesNotThrow(() => { + isAssert.positiveNumber(6); }); - t.notThrows(() => { - assert.positiveNumber(Number.POSITIVE_INFINITY); + assert.doesNotThrow(() => { + isAssert.positiveNumber(1.4); + }); + assert.doesNotThrow(() => { + isAssert.positiveNumber(Number.POSITIVE_INFINITY); }); - t.false(is.positiveNumber(0)); - t.false(is.positiveNumber(-0)); - t.false(is.positiveNumber(-6)); - t.false(is.positiveNumber(-1.4)); - t.false(is.positiveNumber(Number.NEGATIVE_INFINITY)); + assert.strictEqual(is.positiveNumber(0), false); + assert.strictEqual(is.positiveNumber(-0), false); + assert.strictEqual(is.positiveNumber(-6), false); + assert.strictEqual(is.positiveNumber(-1.4), false); + assert.strictEqual(is.positiveNumber(Number.NEGATIVE_INFINITY), false); + assert.strictEqual(is.positiveNumber(Number.NaN), false); - t.throws(() => { - assert.positiveNumber(0); + assert.throws(() => { + isAssert.positiveNumber(0); }); - t.throws(() => { - assert.positiveNumber(-0); + assert.throws(() => { + isAssert.positiveNumber(-0); }); - t.throws(() => { - assert.positiveNumber(-6); + assert.throws(() => { + isAssert.positiveNumber(-6); }); - t.throws(() => { - assert.positiveNumber(-1.4); + assert.throws(() => { + isAssert.positiveNumber(-1.4); }); - t.throws(() => { - assert.positiveNumber(Number.NEGATIVE_INFINITY); + assert.throws(() => { + isAssert.positiveNumber(Number.NEGATIVE_INFINITY); }); }); -test('is.negativeNumber', t => { - t.true(is.negativeNumber(-6)); - t.true(is.negativeNumber(-1.4)); - t.true(is.negativeNumber(Number.NEGATIVE_INFINITY)); +test('is.nan', () => { + assert.ok(is.nan(Number.NaN)); + assert.ok(is.nan(NaN)); // eslint-disable-line unicorn/prefer-number-properties - t.notThrows(() => { - assert.negativeNumber(-6); - }); - t.notThrows(() => { - assert.negativeNumber(-1.4); - }); - t.notThrows(() => { - assert.negativeNumber(Number.NEGATIVE_INFINITY); + assert.doesNotThrow(() => { + isAssert.nan(Number.NaN); }); - t.false(is.negativeNumber(0)); - t.false(is.negativeNumber(-0)); - t.false(is.negativeNumber(6)); - t.false(is.negativeNumber(1.4)); - t.false(is.negativeNumber(Number.POSITIVE_INFINITY)); + assert.strictEqual(is.nan(0), false); + assert.strictEqual(is.nan(-0), false); + assert.strictEqual(is.nan(1), false); + assert.strictEqual(is.nan(Number.POSITIVE_INFINITY), false); + assert.strictEqual(is.nan(Number.NEGATIVE_INFINITY), false); + assert.strictEqual(is.nan('NaN'), false); + assert.strictEqual(is.nan(undefined), false); - t.throws(() => { - assert.negativeNumber(0); + assert.throws(() => { + isAssert.nan(0); }); - t.throws(() => { - assert.negativeNumber(-0); + assert.throws(() => { + isAssert.nan(1); }); - t.throws(() => { - assert.negativeNumber(6); - }); - t.throws(() => { - assert.negativeNumber(1.4); - }); - t.throws(() => { - assert.negativeNumber(Number.POSITIVE_INFINITY); + assert.throws(() => { + isAssert.nan('NaN'); }); }); -test('is.bigint', t => { - testType(t, 'bigint'); -}); +test('is.finiteNumber', () => { + assert.ok(is.finiteNumber(6)); + assert.ok(is.finiteNumber(-6)); + assert.ok(is.finiteNumber(0)); + assert.ok(is.finiteNumber(1.4)); -test('is.boolean', t => { - testType(t, 'boolean'); -}); - -test('is.symbol', t => { - testType(t, 'symbol'); -}); - -test('is.numericString', t => { - testType(t, 'numericString'); - t.false(is.numericString('')); - t.false(is.numericString(' ')); - t.false(is.numericString(' \t\t\n')); - t.false(is.numericString(1)); - t.throws(() => { - assert.numericString(''); + assert.doesNotThrow(() => { + isAssert.finiteNumber(6); }); - t.throws(() => { - assert.numericString(1); + assert.doesNotThrow(() => { + isAssert.finiteNumber(0); + }); + + assert.strictEqual(is.finiteNumber(Number.POSITIVE_INFINITY), false); + assert.strictEqual(is.finiteNumber(Number.NEGATIVE_INFINITY), false); + assert.strictEqual(is.finiteNumber(Number.NaN), false); + + assert.throws(() => { + isAssert.finiteNumber(Number.POSITIVE_INFINITY); + }); + assert.throws(() => { + isAssert.finiteNumber(Number.NEGATIVE_INFINITY); + }); + assert.throws(() => { + isAssert.finiteNumber(Number.NaN); }); }); -test('is.array', t => { - testType(t, 'array', ['emptyArray']); +test('is.negativeNumber', () => { + assert.ok(is.negativeNumber(-6)); + assert.ok(is.negativeNumber(-1.4)); + assert.ok(is.negativeNumber(Number.NEGATIVE_INFINITY)); - t.true(is.array([1, 2, 3], is.number)); - t.false(is.array([1, '2', 3], is.number)); - - t.notThrows(() => { - assert.array([1, 2], assert.number); + assert.doesNotThrow(() => { + isAssert.negativeNumber(-6); + }); + assert.doesNotThrow(() => { + isAssert.negativeNumber(-1.4); + }); + assert.doesNotThrow(() => { + isAssert.negativeNumber(Number.NEGATIVE_INFINITY); }); - t.throws(() => { - assert.array([1, '2'], assert.number); + assert.strictEqual(is.negativeNumber(0), false); + assert.strictEqual(is.negativeNumber(-0), false); + assert.strictEqual(is.negativeNumber(6), false); + assert.strictEqual(is.negativeNumber(1.4), false); + assert.strictEqual(is.negativeNumber(Number.POSITIVE_INFINITY), false); + assert.strictEqual(is.negativeNumber(Number.NaN), false); + + assert.throws(() => { + isAssert.negativeNumber(0); + }); + assert.throws(() => { + isAssert.negativeNumber(-0); + }); + assert.throws(() => { + isAssert.negativeNumber(6); + }); + assert.throws(() => { + isAssert.negativeNumber(1.4); + }); + assert.throws(() => { + isAssert.negativeNumber(Number.POSITIVE_INFINITY); + }); +}); + +test('is.nonNegativeNumber', () => { + assert.ok(is.nonNegativeNumber(0)); + assert.ok(is.nonNegativeNumber(6)); + assert.ok(is.nonNegativeNumber(1.4)); + assert.ok(is.nonNegativeNumber(Number.POSITIVE_INFINITY)); + + assert.doesNotThrow(() => { + isAssert.nonNegativeNumber(0); + }); + assert.doesNotThrow(() => { + isAssert.nonNegativeNumber(6); }); - t.notThrows(() => { + assert.ok(is.nonNegativeNumber(-0)); // -0 >= 0 is true in JavaScript + assert.strictEqual(is.nonNegativeNumber(-6), false); + assert.strictEqual(is.nonNegativeNumber(-1.4), false); + assert.strictEqual(is.nonNegativeNumber(Number.NEGATIVE_INFINITY), false); + assert.strictEqual(is.nonNegativeNumber(Number.NaN), false); + + assert.throws(() => { + isAssert.nonNegativeNumber(-6); + }); + assert.throws(() => { + isAssert.nonNegativeNumber(Number.NEGATIVE_INFINITY); + }); +}); + +test('is.positiveInteger', () => { + assert.ok(is.positiveInteger(1)); + assert.ok(is.positiveInteger(6)); + assert.ok(is.positiveInteger(100)); + + assert.doesNotThrow(() => { + isAssert.positiveInteger(1); + }); + assert.doesNotThrow(() => { + isAssert.positiveInteger(6); + }); + + assert.strictEqual(is.positiveInteger(0), false); + assert.strictEqual(is.positiveInteger(-1), false); + assert.strictEqual(is.positiveInteger(1.5), false); + assert.strictEqual(is.positiveInteger(Number.POSITIVE_INFINITY), false); + assert.strictEqual(is.positiveInteger(Number.NaN), false); + + assert.throws(() => { + isAssert.positiveInteger(0); + }); + assert.throws(() => { + isAssert.positiveInteger(-1); + }); + assert.throws(() => { + isAssert.positiveInteger(1.5); + }); +}); + +test('is.negativeInteger', () => { + assert.ok(is.negativeInteger(-1)); + assert.ok(is.negativeInteger(-6)); + assert.ok(is.negativeInteger(-100)); + + assert.doesNotThrow(() => { + isAssert.negativeInteger(-1); + }); + assert.doesNotThrow(() => { + isAssert.negativeInteger(-6); + }); + + assert.strictEqual(is.negativeInteger(0), false); + assert.strictEqual(is.negativeInteger(-0), false); // -0 < 0 is false in JavaScript + assert.strictEqual(is.negativeInteger(1), false); + assert.strictEqual(is.negativeInteger(-1.5), false); + assert.strictEqual(is.negativeInteger(Number.NEGATIVE_INFINITY), false); + assert.strictEqual(is.negativeInteger(Number.NaN), false); + + assert.throws(() => { + isAssert.negativeInteger(0); + }); + assert.throws(() => { + isAssert.negativeInteger(1); + }); + assert.throws(() => { + isAssert.negativeInteger(-1.5); + }); + assert.throws(() => { + isAssert.negativeInteger(Number.NEGATIVE_INFINITY); + }); +}); + +test('is.nonNegativeInteger', () => { + assert.ok(is.nonNegativeInteger(0)); + assert.ok(is.nonNegativeInteger(1)); + assert.ok(is.nonNegativeInteger(100)); + + assert.doesNotThrow(() => { + isAssert.nonNegativeInteger(0); + }); + assert.doesNotThrow(() => { + isAssert.nonNegativeInteger(1); + }); + + assert.ok(is.nonNegativeInteger(-0)); // -0 >= 0 is true in JavaScript + + assert.strictEqual(is.nonNegativeInteger(-1), false); + assert.strictEqual(is.nonNegativeInteger(1.5), false); + assert.strictEqual(is.nonNegativeInteger(Number.POSITIVE_INFINITY), false); + assert.strictEqual(is.nonNegativeInteger(Number.NaN), false); + + assert.throws(() => { + isAssert.nonNegativeInteger(-1); + }); + assert.throws(() => { + isAssert.nonNegativeInteger(1.5); + }); + assert.throws(() => { + isAssert.nonNegativeInteger(Number.POSITIVE_INFINITY); + }); +}); + +test('is.infinite', () => { + assert.ok(is.infinite(Number.POSITIVE_INFINITY)); + assert.ok(is.infinite(Number.NEGATIVE_INFINITY)); + + assert.doesNotThrow(() => { + isAssert.infinite(Number.POSITIVE_INFINITY); + }); + assert.doesNotThrow(() => { + isAssert.infinite(Number.NEGATIVE_INFINITY); + }); + + assert.strictEqual(is.infinite(0), false); + assert.strictEqual(is.infinite(1), false); + assert.strictEqual(is.infinite(-1), false); + assert.strictEqual(is.infinite(Number.NaN), false); + assert.strictEqual(is.infinite(Number.MAX_VALUE), false); + assert.strictEqual(is.infinite('Infinity'), false); + + assert.throws(() => { + isAssert.infinite(0); + }); + assert.throws(() => { + isAssert.infinite(Number.NaN); + }); +}); + +test('is.numericString supplemental', () => { + assert.strictEqual(is.numericString(''), false); + assert.strictEqual(is.numericString(' '), false); + assert.strictEqual(is.numericString(' \t\t\n'), false); + assert.strictEqual(is.numericString(1), false); + assert.strictEqual(is.numericString(' 5'), false); + assert.strictEqual(is.numericString('5 '), false); + assert.strictEqual(is.numericString(' 5 '), false); + assert.strictEqual(is.numericString('\t3'), false); + assert.throws(() => { + isAssert.numericString(''); + }); + assert.throws(() => { + isAssert.numericString(1); + }); +}); + +test('is.array supplemental', () => { + assert.ok(is.array([1, 2, 3], is.number)); + assert.strictEqual(is.array([1, '2', 3], is.number), false); + + assert.doesNotThrow(() => { + isAssert.array([1, 2], isAssert.number); + }); + + assert.throws(() => { + isAssert.array([1, '2'], isAssert.number); + }); + + assert.doesNotThrow(() => { const x: unknown[] = [1, 2, 3]; - assert.array(x, assert.number); + isAssert.array(x, isAssert.number); x[0]?.toFixed(0); }); - t.notThrows(() => { + assert.doesNotThrow(() => { const x: unknown[] = [1, 2, 3]; if (is.array(x, is.number)) { x[0]?.toFixed(0); } }); - t.throws(() => { - assert.array([1, '2'], assert.number, 'Expected numbers'); - }, {message: /Expected numbers/}); + assert.throws(() => { + isAssert.array([1, '2'], isAssert.number, 'Expected numbers'); + }, /Expected numbers/v); }); -test('is.function', t => { - testType(t, 'function', ['generatorFunction', 'asyncGeneratorFunction', 'asyncFunction', 'boundFunction']); +test('is.arrayOf', () => { + const isStringArray = is.arrayOf(is.string); + assert.ok(isStringArray(['a', 'b', 'c'])); + assert.ok(isStringArray([])); + assert.strictEqual(isStringArray([1, 2, 3]), false); + assert.strictEqual(isStringArray(['a', 1]), false); + assert.strictEqual(isStringArray('not an array'), false); + assert.strictEqual(isStringArray(undefined), false); + + const isNumberArray = is.arrayOf(is.number); + assert.ok(isNumberArray([1, 2, 3])); + assert.strictEqual(isNumberArray([1, '2']), false); }); -test('is.boundFunction', t => { - t.false(is.boundFunction(function () {})); // eslint-disable-line prefer-arrow-callback +test('is.oneOf', () => { + const isDirection = is.oneOf(['north', 'south', 'east', 'west'] as const); + assert.ok(isDirection('north')); + assert.ok(isDirection('west')); + assert.strictEqual(isDirection('up'), false); + assert.strictEqual(isDirection(1), false); + assert.strictEqual(isDirection(undefined), false); - t.throws(() => { - assert.boundFunction(function () {}); // eslint-disable-line prefer-arrow-callback + const isSmallNumber = is.oneOf([1, 2, 3] as const); + assert.ok(isSmallNumber(1)); + assert.strictEqual(isSmallNumber(4), false); + + // Empty values array always returns false + const isNever = is.oneOf([] as const); + assert.strictEqual(isNever('anything'), false); + + // Array.includes uses SameValueZero, so NaN matches NaN (unlike ===) + const isNanValue = is.oneOf([Number.NaN] as const); + assert.ok(isNanValue(Number.NaN)); +}); + +test('is.boundFunction supplemental', () => { + assert.strictEqual(is.boundFunction(function () {}), false); // eslint-disable-line prefer-arrow-callback + + assert.throws(() => { + isAssert.boundFunction(function () {}); // eslint-disable-line prefer-arrow-callback }); }); -test('is.buffer', t => { - testType(t, 'buffer'); -}); - -test('is.blob', t => { - testType(t, 'blob'); -}); - -test('is.object', t => { - const testData = types.get('object'); - - if (testData === undefined) { - t.fail('is.object not defined'); - - return; - } - - for (const element of testData.fixtures) { - t.true(is.object(element)); - t.notThrows(() => { - assert.object(element); - }); - } -}); - -test('is.regExp', t => { - testType(t, 'regExp'); -}); - -test('is.date', t => { - testType(t, 'date'); -}); - -test('is.error', t => { - testType(t, 'error'); -}); - -test('is.nativePromise', t => { - testType(t, 'nativePromise'); -}); - -test('is.promise', t => { - testType(t, 'promise', ['nativePromise']); -}); - -test('is.asyncFunction', t => { - testType(t, 'asyncFunction', ['function']); - +test('is.asyncFunction supplemental', () => { const fixture = async () => {}; if (is.asyncFunction(fixture)) { - t.true(is.function(fixture().then)); + assert.ok(is.function(fixture().then)); - t.notThrows(() => { - assert.function(fixture().then); + assert.doesNotThrow(() => { + isAssert.function(fixture().then); }); } }); -test('is.generator', t => { - testType(t, 'generator'); -}); - -test('is.asyncGenerator', t => { - testType(t, 'asyncGenerator'); - +test('is.asyncGenerator supplemental', () => { const fixture = (async function * () { yield 4; })(); if (is.asyncGenerator(fixture)) { - t.true(is.function(fixture.next)); + assert.ok(is.function(fixture.next)); } }); -test('is.generatorFunction', t => { - testType(t, 'generatorFunction', ['function']); -}); - -test('is.asyncGeneratorFunction', t => { - testType(t, 'asyncGeneratorFunction', ['function']); - +test('is.asyncGeneratorFunction supplemental', () => { const fixture = async function * () { yield 4; }; if (is.asyncGeneratorFunction(fixture)) { - t.true(is.function(fixture().next)); + assert.ok(is.function(fixture().next)); } }); -test('is.map', t => { - testType(t, 'map', ['emptyMap']); -}); - -test('is.set', t => { - testType(t, 'set', ['emptySet']); -}); - -test('is.weakMap', t => { - testType(t, 'weakMap'); -}); - -test('is.weakSet', t => { - testType(t, 'weakSet'); -}); - -test('is.weakRef', t => { - testType(t, 'weakRef'); -}); - -test('is.int8Array', t => { - testType(t, 'int8Array'); -}); - -test('is.uint8Array', t => { - testType(t, 'uint8Array', ['buffer']); -}); - -test('is.uint8ClampedArray', t => { - testType(t, 'uint8ClampedArray'); -}); - -test('is.int16Array', t => { - testType(t, 'int16Array'); -}); - -test('is.uint16Array', t => { - testType(t, 'uint16Array'); -}); - -test('is.int32Array', t => { - testType(t, 'int32Array'); -}); - -test('is.uint32Array', t => { - testType(t, 'uint32Array'); -}); - -test('is.float32Array', t => { - testType(t, 'float32Array'); -}); - -test('is.float64Array', t => { - testType(t, 'float64Array'); -}); - -test('is.bigInt64Array', t => { - testType(t, 'bigInt64Array'); -}); - -test('is.bigUint64Array', t => { - testType(t, 'bigUint64Array'); -}); - -test('is.arrayBuffer', t => { - testType(t, 'arrayBuffer'); -}); - -test('is.dataView', t => { - testType(t, 'dataView'); -}); - -test('is.enumCase', t => { +test('is.enumCase', () => { enum NonNumericalEnum { Key1 = 'key1', Key2 = 'key2', } - t.true(is.enumCase('key1', NonNumericalEnum)); - t.notThrows(() => { - assert.enumCase('key1', NonNumericalEnum); + enum NumericKeyStringEnum { + // eslint-disable-next-line @stylistic/quote-props + '0' = 'zero', + '01' = 'padded', + } + + assert.ok(is.enumCase('key1', NonNumericalEnum)); + assert.doesNotThrow(() => { + isAssert.enumCase('key1', NonNumericalEnum); }); - t.false(is.enumCase('invalid', NonNumericalEnum)); - t.throws(() => { - assert.enumCase('invalid', NonNumericalEnum); + assert.strictEqual(is.enumCase('invalid', NonNumericalEnum), false); + assert.throws(() => { + isAssert.enumCase('invalid', NonNumericalEnum); }); + + assert.ok(is.enumCase('zero', NumericKeyStringEnum)); + assert.ok(is.enumCase('padded', NumericKeyStringEnum)); + assert.doesNotThrow(() => { + isAssert.enumCase('zero', NumericKeyStringEnum); + }); + assert.doesNotThrow(() => { + isAssert.enumCase('padded', NumericKeyStringEnum); + }); + + enum NumericalEnum { + Key1 = 0, + Key2 = 1, + } + + assert.ok(is.enumCase(0, NumericalEnum)); + assert.ok(is.enumCase(1, NumericalEnum)); + assert.strictEqual(is.enumCase('Key1', NumericalEnum), false); + assert.strictEqual(is.enumCase('Key2', NumericalEnum), false); + assert.doesNotThrow(() => { + isAssert.enumCase(0, NumericalEnum); + }); + assert.throws(() => { + isAssert.enumCase('Key1', NumericalEnum); + }); + + enum HeterogeneousEnum { + A = 1, + B = 'hello', + } + + assert.ok(is.enumCase(1, HeterogeneousEnum)); + assert.ok(is.enumCase('hello', HeterogeneousEnum)); + assert.strictEqual(is.enumCase('A', HeterogeneousEnum), false); }); -test('is.directInstanceOf', t => { +test('is.directInstanceOf', () => { const error = new Error('fixture'); const errorSubclass = new ErrorSubclassFixture(); - t.true(is.directInstanceOf(error, Error)); - t.true(is.directInstanceOf(errorSubclass, ErrorSubclassFixture)); - t.notThrows(() => { - assert.directInstanceOf(error, Error); + assert.ok(is.directInstanceOf(error, Error)); + assert.ok(is.directInstanceOf(errorSubclass, ErrorSubclassFixture)); + assert.doesNotThrow(() => { + isAssert.directInstanceOf(error, Error); }); - t.notThrows(() => { - assert.directInstanceOf(errorSubclass, ErrorSubclassFixture); + assert.doesNotThrow(() => { + isAssert.directInstanceOf(errorSubclass, ErrorSubclassFixture); }); - t.false(is.directInstanceOf(error, ErrorSubclassFixture)); - t.false(is.directInstanceOf(errorSubclass, Error)); - t.throws(() => { - assert.directInstanceOf(error, ErrorSubclassFixture); + assert.strictEqual(is.directInstanceOf(error, ErrorSubclassFixture), false); + assert.strictEqual(is.directInstanceOf(errorSubclass, Error), false); + assert.throws(() => { + isAssert.directInstanceOf(error, ErrorSubclassFixture); }); - t.throws(() => { - assert.directInstanceOf(errorSubclass, Error); + assert.throws(() => { + isAssert.directInstanceOf(errorSubclass, Error); }); - t.false(is.directInstanceOf(undefined, Error)); - t.false(is.directInstanceOf(null, Error)); + assert.strictEqual(is.directInstanceOf(undefined, Error), false); + assert.strictEqual(is.directInstanceOf(null, Error), false); }); -test('is.urlInstance', t => { +test('is.urlInstance', () => { const url = new URL('https://example.com'); - t.true(is.urlInstance(url)); - t.false(is.urlInstance({})); - t.false(is.urlInstance(undefined)); - t.false(is.urlInstance(null)); + assert.ok(is.urlInstance(url)); + assert.strictEqual(is.urlInstance({}), false); + assert.strictEqual(is.urlInstance(undefined), false); + assert.strictEqual(is.urlInstance(null), false); - t.notThrows(() => { - assert.urlInstance(url); + assert.doesNotThrow(() => { + isAssert.urlInstance(url); }); - t.throws(() => { - assert.urlInstance({}); + assert.throws(() => { + isAssert.urlInstance({}); }); - t.throws(() => { - assert.urlInstance(undefined); + assert.throws(() => { + isAssert.urlInstance(undefined); }); - t.throws(() => { - assert.urlInstance(null); + assert.throws(() => { + isAssert.urlInstance(null); }); }); -test('is.urlString', t => { +test('is.urlString', () => { const url = 'https://example.com'; - t.true(is.urlString(url)); - t.false(is.urlString(new URL(url))); - t.false(is.urlString({})); - t.false(is.urlString(undefined)); - t.false(is.urlString(null)); + assert.ok(is.urlString(url)); + assert.strictEqual(is.urlString(new URL(url)), false); + assert.strictEqual(is.urlString({}), false); + assert.strictEqual(is.urlString(undefined), false); + assert.strictEqual(is.urlString(null), false); - t.notThrows(() => { - assert.urlString(url); + assert.doesNotThrow(() => { + isAssert.urlString(url); }); - t.throws(() => { - assert.urlString(new URL(url)); + assert.throws(() => { + isAssert.urlString(new URL(url)); }); - t.throws(() => { - assert.urlString({}); + assert.throws(() => { + isAssert.urlString({}); }); - t.throws(() => { - assert.urlString(undefined); + assert.throws(() => { + isAssert.urlString(undefined); }); - t.throws(() => { - assert.urlString(null); + assert.throws(() => { + isAssert.urlString(null); }); }); -test('is.truthy', t => { - t.true(is.truthy('unicorn')); - t.true(is.truthy('🦄')); - t.true(is.truthy(new Set())); - t.true(is.truthy(Symbol('🦄'))); - t.true(is.truthy(true)); - t.true(is.truthy(1)); - // Disabled until TS supports it for an ESnnnn target. - // t.true(is.truthy(1n)); - t.true(is.truthy(BigInt(1))); +// Type test for urlString narrowing fix (issue #212) +// This test demonstrates that the fix allows proper type narrowing in both branches +(() => { + const value: unknown = 'test'; - t.notThrows(() => { - assert.truthy('unicorn'); + if (is.urlString(value)) { + // ✅ In true branch: value is narrowed to UrlString + expectTypeOf(value).toEqualTypeOf(); + expectTypeOf(value).toMatchTypeOf(); + } else { + // ✅ In false branch: value remains unknown (not incorrectly narrowed) + expectTypeOf(value).toEqualTypeOf(); + + // ✅ Manual narrowing to string still works + if (typeof value === 'string') { + expectTypeOf(value).toEqualTypeOf(); + } + } +})(); + +// Type test for is.nan branded-type narrowing +(() => { + const value: unknown = Number.NaN; + + if (is.nan(value)) { + // ✅ In true branch: value is narrowed to the branded NaN type + expectTypeOf(value).toEqualTypeOf(); + expectTypeOf(value).toMatchTypeOf(); + } else { + // ✅ In false branch: value remains unknown (not incorrectly narrowed) + expectTypeOf(value).toEqualTypeOf(); + } +})(); + +test('is.truthy', () => { + assert.ok(is.truthy('unicorn')); + assert.ok(is.truthy('🦄')); + assert.ok(is.truthy(new Set())); + assert.ok(is.truthy(Symbol('🦄'))); + assert.ok(is.truthy(true)); + assert.ok(is.truthy(1)); + assert.ok(is.truthy(1n)); + + assert.doesNotThrow(() => { + isAssert.truthy('unicorn'); }); - t.notThrows(() => { - assert.truthy('🦄'); + assert.doesNotThrow(() => { + isAssert.truthy('🦄'); }); - t.notThrows(() => { - assert.truthy(new Set()); + assert.doesNotThrow(() => { + isAssert.truthy(new Set()); }); - t.notThrows(() => { - assert.truthy(Symbol('🦄')); + assert.doesNotThrow(() => { + isAssert.truthy(Symbol('🦄')); }); - t.notThrows(() => { - assert.truthy(true); + assert.doesNotThrow(() => { + isAssert.truthy(true); }); - t.notThrows(() => { - assert.truthy(1); + assert.doesNotThrow(() => { + isAssert.truthy(1); }); - t.notThrows(() => { - assert.truthy(1n); + assert.doesNotThrow(() => { + isAssert.truthy(1n); }); - t.notThrows(() => { - assert.truthy(BigInt(1)); - }); - - // Checks that `assert.truthy` narrow downs boolean type to `true`. + // Checks that `isAssert.truthy` narrow downs boolean type to `true`. { const booleans = [true, false]; const function_ = (value: true) => value; - assert.truthy(booleans[0]); + isAssert.truthy(booleans[0]); function_(booleans[0]); } - // Checks that `assert.truthy` excludes zero value from number type. + // Checks that `isAssert.truthy` excludes zero value from number type. { const bits: Array<0 | 1> = [1, 0, -0]; const function_ = (value: 1) => value; - assert.truthy(bits[0]); + isAssert.truthy(bits[0]); function_(bits[0]); } - // Checks that `assert.truthy` excludes zero value from bigint type. + // Checks that `isAssert.truthy` excludes zero value from bigint type. { const bits: Array<0n | 1n> = [1n, 0n, -0n]; const function_ = (value: 1n) => value; - assert.truthy(bits[0]); + isAssert.truthy(bits[0]); function_(bits[0]); } - // Checks that `assert.truthy` excludes empty string from string type. + // Checks that `isAssert.truthy` excludes empty string from string type. { const strings: Array<'nonEmpty' | ''> = ['nonEmpty', '']; const function_ = (value: 'nonEmpty') => value; - assert.truthy(strings[0]); + isAssert.truthy(strings[0]); function_(strings[0]); } - // Checks that `assert.truthy` excludes undefined from mixed type. + // Checks that `isAssert.truthy` excludes undefined from mixed type. { const maybeUndefineds = ['🦄', undefined]; const function_ = (value: string) => value; - assert.truthy(maybeUndefineds[0]); + isAssert.truthy(maybeUndefineds[0]); function_(maybeUndefineds[0]); } - // Checks that `assert.truthy` excludes null from mixed type. + // Checks that `isAssert.truthy` excludes null from mixed type. { const maybeNulls = ['🦄', null]; const function_ = (value: string) => value; - assert.truthy(maybeNulls[0]); + isAssert.truthy(maybeNulls[0]); function_(maybeNulls[0]); } }); -test('is.falsy', t => { - t.true(is.falsy(false)); - t.true(is.falsy(0)); - t.true(is.falsy('')); - t.true(is.falsy(null)); - t.true(is.falsy(undefined)); - t.true(is.falsy(Number.NaN)); - t.true(is.falsy(0n)); - t.true(is.falsy(BigInt(0))); +test('is.falsy', () => { + assert.ok(is.falsy(false)); + assert.ok(is.falsy(0)); + assert.ok(is.falsy('')); + assert.ok(is.falsy(null)); + assert.ok(is.falsy(undefined)); + assert.ok(is.falsy(Number.NaN)); + assert.ok(is.falsy(0n)); - t.notThrows(() => { - assert.falsy(false); + assert.doesNotThrow(() => { + isAssert.falsy(false); }); - t.notThrows(() => { - assert.falsy(0); + assert.doesNotThrow(() => { + isAssert.falsy(0); }); - t.notThrows(() => { - assert.falsy(''); + assert.doesNotThrow(() => { + isAssert.falsy(''); }); - t.notThrows(() => { - assert.falsy(null); + assert.doesNotThrow(() => { + isAssert.falsy(null); }); - t.notThrows(() => { - assert.falsy(undefined); + assert.doesNotThrow(() => { + isAssert.falsy(undefined); }); - t.notThrows(() => { - assert.falsy(Number.NaN); + assert.doesNotThrow(() => { + isAssert.falsy(Number.NaN); }); - t.notThrows(() => { - assert.falsy(0n); + assert.doesNotThrow(() => { + isAssert.falsy(0n); }); - t.notThrows(() => { - assert.falsy(BigInt(0)); - }); - - // Checks that `assert.falsy` narrow downs boolean type to `false`. + // Checks that `isAssert.falsy` narrow downs boolean type to `false`. { const booleans = [false, true]; const function_ = (value?: false) => value; - assert.falsy(booleans[0]); + isAssert.falsy(booleans[0]); function_(booleans[0]); } - // Checks that `assert.falsy` narrow downs number type to `0`. + // Checks that `isAssert.falsy` narrow downs number type to `0`. { const bits = [0, -0, 1]; const function_ = (value?: 0) => value; - assert.falsy(bits[0]); + isAssert.falsy(bits[0]); function_(bits[0]); - assert.falsy(bits[1]); + isAssert.falsy(bits[1]); function_(bits[1]); } - // Checks that `assert.falsy` narrow downs bigint type to `0n`. + // Checks that `isAssert.falsy` narrow downs bigint type to `0n`. { const bits = [0n, -0n, 1n]; const function_ = (value?: 0n) => value; - assert.falsy(bits[0]); + isAssert.falsy(bits[0]); function_(bits[0]); - assert.falsy(bits[1]); + isAssert.falsy(bits[1]); function_(bits[1]); } - // Checks that `assert.falsy` narrow downs string type to empty string. + // Checks that `isAssert.falsy` narrow downs string type to empty string. { const strings = ['', 'nonEmpty']; const function_ = (value?: '') => value; - assert.falsy(strings[0]); + isAssert.falsy(strings[0]); function_(strings[0]); } - // Checks that `assert.falsy` can narrow down mixed type to undefined. + // Checks that `isAssert.falsy` can narrow down mixed type to undefined. { const maybeUndefineds = [undefined, Symbol('🦄')]; const function_ = (value: undefined) => value; - assert.falsy(maybeUndefineds[0]); + isAssert.falsy(maybeUndefineds[0]); function_(maybeUndefineds[0]); } - // Checks that `assert.falsy` can narrow down mixed type to null. + // Checks that `isAssert.falsy` can narrow down mixed type to null. { const maybeNulls = [null, Symbol('🦄')]; - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-restricted-types const function_ = (value?: null) => value; - assert.falsy(maybeNulls[0]); + isAssert.falsy(maybeNulls[0]); function_(maybeNulls[0]); } }); -test('is.nan', t => { - testType(t, 'nan'); -}); - -test('is.nullOrUndefined', t => { - testType(t, 'nullOrUndefined', ['undefined', 'null']); -}); - -test('is.primitive', t => { +test('is.primitive', () => { const primitives: Primitive[] = [ undefined, null, @@ -1248,142 +1269,140 @@ test('is.primitive', t => { true, false, Symbol('🦄'), - // Disabled until TS supports it for an ESnnnn target. - // 6n + 6n, ]; for (const element of primitives) { - t.true(is.primitive(element)); - t.notThrows(() => { - assert.primitive(element); + assert.ok(is.primitive(element)); + assert.doesNotThrow(() => { + isAssert.primitive(element); }); } }); -test('is.integer', t => { - testType(t, 'integer', ['number', 'safeInteger']); - t.false(is.integer(1.4)); - t.throws(() => { - assert.integer(1.4); +test('is.integer supplemental', () => { + assert.strictEqual(is.integer(1.4), false); + assert.throws(() => { + isAssert.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); +test('is.safeInteger supplemental', () => { + assert.strictEqual(is.safeInteger(2 ** 53), false); + assert.strictEqual(is.safeInteger(-(2 ** 53)), false); + assert.throws(() => { + isAssert.safeInteger(2 ** 53); }); - t.throws(() => { - assert.safeInteger(-(2 ** 53)); + assert.throws(() => { + isAssert.safeInteger(-(2 ** 53)); }); }); -test('is.plainObject', t => { - testType(t, 'plainObject', ['object', 'promise']); -}); +test('is.iterable', () => { + assert.ok(is.iterable('')); + assert.ok(is.iterable([])); + assert.ok(is.iterable(new Map())); + assert.strictEqual(is.iterable(null), false); + assert.strictEqual(is.iterable(undefined), false); + assert.strictEqual(is.iterable(0), false); + assert.strictEqual(is.iterable(Number.NaN), false); + assert.strictEqual(is.iterable(Number.POSITIVE_INFINITY), false); + assert.strictEqual(is.iterable({}), false); -test('is.iterable', t => { - t.true(is.iterable('')); - t.true(is.iterable([])); - t.true(is.iterable(new Map())); - t.false(is.iterable(null)); - t.false(is.iterable(undefined)); - t.false(is.iterable(0)); - t.false(is.iterable(Number.NaN)); - t.false(is.iterable(Number.POSITIVE_INFINITY)); - t.false(is.iterable({})); - - t.notThrows(() => { - assert.iterable(''); + assert.doesNotThrow(() => { + isAssert.iterable(''); }); - t.notThrows(() => { - assert.iterable([]); + assert.doesNotThrow(() => { + isAssert.iterable([]); }); - t.notThrows(() => { - assert.iterable(new Map()); + assert.doesNotThrow(() => { + isAssert.iterable(new Map()); }); - t.throws(() => { - assert.iterable(null); + assert.throws(() => { + isAssert.iterable(null); }); - t.throws(() => { - assert.iterable(undefined); + assert.throws(() => { + isAssert.iterable(undefined); }); - t.throws(() => { - assert.iterable(0); + assert.throws(() => { + isAssert.iterable(0); }); - t.throws(() => { - assert.iterable(Number.NaN); + assert.throws(() => { + isAssert.iterable(Number.NaN); }); - t.throws(() => { - assert.iterable(Number.POSITIVE_INFINITY); + assert.throws(() => { + isAssert.iterable(Number.POSITIVE_INFINITY); }); - t.throws(() => { - assert.iterable({}); + assert.throws(() => { + isAssert.iterable({}); }); }); -test('is.asyncIterable', t => { - t.true(is.asyncIterable({ +test('is.asyncIterable', () => { + assert.ok(is.asyncIterable({ [Symbol.asyncIterator]() {}, })); - t.false(is.asyncIterable(null)); - t.false(is.asyncIterable(undefined)); - t.false(is.asyncIterable(0)); - t.false(is.asyncIterable(Number.NaN)); - t.false(is.asyncIterable(Number.POSITIVE_INFINITY)); - t.false(is.asyncIterable({})); + assert.strictEqual(is.asyncIterable(null), false); + assert.strictEqual(is.asyncIterable(undefined), false); + assert.strictEqual(is.asyncIterable(0), false); + assert.strictEqual(is.asyncIterable(Number.NaN), false); + assert.strictEqual(is.asyncIterable(Number.POSITIVE_INFINITY), false); + assert.strictEqual(is.asyncIterable({}), false); - t.notThrows(() => { - assert.asyncIterable({ + assert.doesNotThrow(() => { + isAssert.asyncIterable({ [Symbol.asyncIterator]() {}, }); }); - t.throws(() => { - assert.asyncIterable(null); + assert.throws(() => { + isAssert.asyncIterable(null); }); - t.throws(() => { - assert.asyncIterable(undefined); + assert.throws(() => { + isAssert.asyncIterable(undefined); }); - t.throws(() => { - assert.asyncIterable(0); + assert.throws(() => { + isAssert.asyncIterable(0); }); - t.throws(() => { - assert.asyncIterable(Number.NaN); + assert.throws(() => { + isAssert.asyncIterable(Number.NaN); }); - t.throws(() => { - assert.asyncIterable(Number.POSITIVE_INFINITY); + assert.throws(() => { + isAssert.asyncIterable(Number.POSITIVE_INFINITY); }); - t.throws(() => { - assert.asyncIterable({}); + assert.throws(() => { + isAssert.asyncIterable({}); }); }); -test('is.class', t => { +test('is.class', () => { class Foo {} // eslint-disable-line @typescript-eslint/no-extraneous-class + // Note: Using new Function to test a minified class (no whitespace in source) + const minifiedClass = new Function('return class{};'); // eslint-disable-line no-new-func + const classDeclarations = [ Foo, class Bar extends Foo {}, + minifiedClass(), ]; for (const classDeclaration of classDeclarations) { - t.true(is.class(classDeclaration)); + assert.ok(is.class(classDeclaration)); - t.notThrows(() => { - assert.class(classDeclaration); + assert.doesNotThrow(() => { + isAssert.class(classDeclaration); }); } }); -test('is.typedArray', t => { +test('is.typedArray', () => { const typedArrays: TypedArray[] = [ new Int8Array(), new Uint8Array(), new Uint8ClampedArray(), + new Int16Array(), new Uint16Array(), new Int32Array(), new Uint32Array(), @@ -1394,98 +1413,98 @@ test('is.typedArray', t => { ]; for (const item of typedArrays) { - t.true(is.typedArray(item)); + assert.ok(is.typedArray(item)); - t.notThrows(() => { - assert.typedArray(item); + assert.doesNotThrow(() => { + isAssert.typedArray(item); }); } - t.false(is.typedArray(new ArrayBuffer(1))); - t.false(is.typedArray([])); - t.false(is.typedArray({})); + assert.strictEqual(is.typedArray(new ArrayBuffer(1)), false); + assert.strictEqual(is.typedArray([]), false); + assert.strictEqual(is.typedArray({}), false); - t.throws(() => { - assert.typedArray(new ArrayBuffer(1)); + assert.throws(() => { + isAssert.typedArray(new ArrayBuffer(1)); }); - t.throws(() => { - assert.typedArray([]); + assert.throws(() => { + isAssert.typedArray([]); }); - t.throws(() => { - assert.typedArray({}); + assert.throws(() => { + isAssert.typedArray({}); }); }); -test('is.arrayLike', t => { +test('is.arrayLike', () => { (function () { - t.true(is.arrayLike(arguments)); // eslint-disable-line prefer-rest-params + assert.ok(is.arrayLike(arguments)); // eslint-disable-line prefer-rest-params })(); - t.true(is.arrayLike([])); - t.true(is.arrayLike('unicorn')); + assert.ok(is.arrayLike([])); + assert.ok(is.arrayLike('unicorn')); - t.false(is.arrayLike({})); - t.false(is.arrayLike(() => {})); - t.false(is.arrayLike(new Map())); + assert.strictEqual(is.arrayLike({}), false); + assert.strictEqual(is.arrayLike(() => {}), false); + assert.strictEqual(is.arrayLike(new Map()), false); (function () { - t.notThrows(function () { - assert.arrayLike(arguments); // eslint-disable-line prefer-rest-params + assert.doesNotThrow(function () { + isAssert.arrayLike(arguments); // eslint-disable-line prefer-rest-params }); })(); - t.notThrows(() => { - assert.arrayLike([]); + assert.doesNotThrow(() => { + isAssert.arrayLike([]); }); - t.notThrows(() => { - assert.arrayLike('unicorn'); + assert.doesNotThrow(() => { + isAssert.arrayLike('unicorn'); }); - t.throws(() => { - assert.arrayLike({}); + assert.throws(() => { + isAssert.arrayLike({}); }); - t.throws(() => { - assert.arrayLike(() => {}); + assert.throws(() => { + isAssert.arrayLike(() => {}); }); - t.throws(() => { - assert.arrayLike(new Map()); + assert.throws(() => { + isAssert.arrayLike(new Map()); }); }); -test('is.tupleLike', t => { +test('is.tupleLike', () => { (function () { - t.false(is.tupleLike(arguments, [])); // eslint-disable-line prefer-rest-params + assert.strictEqual(is.tupleLike(arguments, []), false); // eslint-disable-line prefer-rest-params })(); - t.true(is.tupleLike([], [])); - t.true(is.tupleLike([1, '2', true, {}, [], undefined, null], [is.number, is.string, is.boolean, is.object, is.array, is.undefined, is.nullOrUndefined])); - t.false(is.tupleLike('unicorn', [is.string])); + assert.ok(is.tupleLike([], [])); + assert.ok(is.tupleLike([1, '2', true, {}, [], undefined, null], [is.number, is.string, is.boolean, is.object, is.array, is.undefined, is.nullOrUndefined])); + assert.strictEqual(is.tupleLike('unicorn', [is.string]), false); - t.false(is.tupleLike({}, [])); - t.false(is.tupleLike(() => {}, [is.function])); - t.false(is.tupleLike(new Map(), [is.map])); + assert.strictEqual(is.tupleLike({}, []), false); + assert.strictEqual(is.tupleLike(() => {}, [is.function]), false); + assert.strictEqual(is.tupleLike(new Map(), [is.map]), false); (function () { - t.throws(function () { - assert.tupleLike(arguments, []); // eslint-disable-line prefer-rest-params + assert.throws(function () { + isAssert.tupleLike(arguments, []); // eslint-disable-line prefer-rest-params }); })(); - t.notThrows(() => { - assert.tupleLike([], []); + assert.doesNotThrow(() => { + isAssert.tupleLike([], []); }); - t.throws(() => { - assert.tupleLike('unicorn', [is.string]); + assert.throws(() => { + isAssert.tupleLike('unicorn', [is.string]); }); - t.throws(() => { - assert.tupleLike({}, [is.object]); + assert.throws(() => { + isAssert.tupleLike({}, [is.object]); }); - t.throws(() => { - assert.tupleLike(() => {}, [is.function]); + assert.throws(() => { + isAssert.tupleLike(() => {}, [is.function]); }); - t.throws(() => { - assert.tupleLike(new Map(), [is.map]); + assert.throws(() => { + isAssert.tupleLike(new Map(), [is.map]); }); { @@ -1511,128 +1530,135 @@ test('is.tupleLike', t => { { const tuple = [1, '1', true, null, undefined]; - if (is.tupleLike(tuple, [is.number, is.string, is.boolean, is.undefined, is.null])) { + if (is.tupleLike(tuple, [is.number, is.string, is.boolean, is.null, is.undefined])) { const numericValue = tuple[0]; const stringValue = tuple[1]; const booleanValue = tuple[2]; - const undefinedValue = tuple[3]; - const nullValue = tuple[4]; + const nullValue = tuple[3]; + const undefinedValue = tuple[4]; expectTypeOf(numericValue).toEqualTypeOf(); expectTypeOf(stringValue).toEqualTypeOf(); expectTypeOf(booleanValue).toEqualTypeOf(); - expectTypeOf(undefinedValue).toEqualTypeOf(); - // eslint-disable-next-line @typescript-eslint/ban-types + // eslint-disable-next-line @typescript-eslint/no-restricted-types expectTypeOf(nullValue).toEqualTypeOf(); + expectTypeOf(undefinedValue).toEqualTypeOf(); } } }); -test('is.inRange', t => { +test('is.inRange', () => { const x = 3; - t.true(is.inRange(x, [0, 5])); - t.true(is.inRange(x, [5, 0])); - t.true(is.inRange(x, [-5, 5])); - t.true(is.inRange(x, [5, -5])); - t.false(is.inRange(x, [4, 8])); - t.true(is.inRange(-7, [-5, -10])); - t.true(is.inRange(-5, [-5, -10])); - t.true(is.inRange(-10, [-5, -10])); + assert.ok(is.inRange(x, [0, 5])); + assert.ok(is.inRange(x, [5, 0])); + assert.ok(is.inRange(x, [-5, 5])); + assert.ok(is.inRange(x, [5, -5])); + assert.strictEqual(is.inRange(x, [4, 8]), false); + assert.ok(is.inRange(-7, [-5, -10])); + assert.ok(is.inRange(-5, [-5, -10])); + assert.ok(is.inRange(-10, [-5, -10])); - t.true(is.inRange(x, 10)); - t.true(is.inRange(0, 0)); - t.true(is.inRange(-2, -3)); - t.false(is.inRange(x, 2)); - t.false(is.inRange(-3, -2)); + assert.ok(is.inRange(x, 10)); + assert.ok(is.inRange(0, 0)); + assert.ok(is.inRange(-2, -3)); + assert.strictEqual(is.inRange(x, 2), false); + assert.strictEqual(is.inRange(-3, -2), false); - t.throws(() => { + assert.throws(() => { // @ts-expect-error invalid argument is.inRange(0, []); }); - t.throws(() => { + assert.throws(() => { // @ts-expect-error invalid argument is.inRange(0, [5]); }); - t.throws(() => { + assert.throws(() => { // @ts-expect-error invalid argument is.inRange(0, [1, 2, 3]); }); - t.notThrows(() => { - assert.inRange(x, [0, 5]); + assert.throws(() => { + is.inRange(5, [Number.NaN, 10]); + }, TypeError); + + assert.throws(() => { + is.inRange(5, [0, Number.NaN]); + }, TypeError); + + assert.doesNotThrow(() => { + isAssert.inRange(x, [0, 5]); }); - t.notThrows(() => { - assert.inRange(x, [5, 0]); + assert.doesNotThrow(() => { + isAssert.inRange(x, [5, 0]); }); - t.notThrows(() => { - assert.inRange(x, [-5, 5]); + assert.doesNotThrow(() => { + isAssert.inRange(x, [-5, 5]); }); - t.notThrows(() => { - assert.inRange(x, [5, -5]); + assert.doesNotThrow(() => { + isAssert.inRange(x, [5, -5]); }); - t.throws(() => { - assert.inRange(x, [4, 8]); + assert.throws(() => { + isAssert.inRange(x, [4, 8]); }); - t.notThrows(() => { - assert.inRange(-7, [-5, -10]); + assert.doesNotThrow(() => { + isAssert.inRange(-7, [-5, -10]); }); - t.notThrows(() => { - assert.inRange(-5, [-5, -10]); + assert.doesNotThrow(() => { + isAssert.inRange(-5, [-5, -10]); }); - t.notThrows(() => { - assert.inRange(-10, [-5, -10]); + assert.doesNotThrow(() => { + isAssert.inRange(-10, [-5, -10]); }); - t.notThrows(() => { - assert.inRange(x, 10); + assert.doesNotThrow(() => { + isAssert.inRange(x, 10); }); - t.notThrows(() => { - assert.inRange(0, 0); + assert.doesNotThrow(() => { + isAssert.inRange(0, 0); }); - t.notThrows(() => { - assert.inRange(-2, -3); + assert.doesNotThrow(() => { + isAssert.inRange(-2, -3); }); - t.throws(() => { - assert.inRange(x, 2); + assert.throws(() => { + isAssert.inRange(x, 2); }); - t.throws(() => { - assert.inRange(-3, -2); + assert.throws(() => { + isAssert.inRange(-3, -2); }); - t.throws(() => { + assert.throws(() => { // @ts-expect-error invalid argument - assert.inRange(0, []); + isAssert.inRange(0, []); }); - t.throws(() => { + assert.throws(() => { // @ts-expect-error invalid argument - assert.inRange(0, [5]); + isAssert.inRange(0, [5]); }); - t.throws(() => { + assert.throws(() => { // @ts-expect-error invalid argument - assert.inRange(0, [1, 2, 3]); + isAssert.inRange(0, [1, 2, 3]); }); }); -test('is.htmlElement', t => { - testType(t, 'htmlElement'); - t.false(is.htmlElement({nodeType: 1, nodeName: 'div'})); - t.throws(() => { - assert.htmlElement({nodeType: 1, nodeName: 'div'}); +test('is.htmlElement supplemental', () => { + assert.strictEqual(is.htmlElement({nodeType: 1, nodeName: 'div'}), false); + assert.throws(() => { + isAssert.htmlElement({nodeType: 1, nodeName: 'div'}); }); const tagNames = [ @@ -1646,71 +1672,70 @@ test('is.htmlElement', t => { for (const tagName of tagNames) { const element = document.createElement(tagName); - t.is(is(element), 'HTMLElement'); + assert.strictEqual(is(element), 'HTMLElement'); + } + + const nonHtmlElements = [ + document.createTextNode('data'), + document.createProcessingInstruction('xml-stylesheet', 'href="mycss.css" type="text/css"'), + document.createComment('This is a comment'), + document, + document.implementation.createDocumentType('svg:svg', '-//W3C//DTD SVG 1.1//EN', 'https://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd'), + document.createDocumentFragment(), + ] as const; + + for (const element of nonHtmlElements) { + assert.throws(() => { + isAssert.htmlElement(element); + }); } }); -test('is.observable', t => { - testType(t, 'observable'); -}); - -test('is.nodeStream', t => { - testType(t, 'nodeStream'); -}); - -test('is.infinite', t => { - testType(t, 'infinite', ['number']); -}); - -test('is.evenInteger', t => { +test('is.evenInteger', () => { for (const element of [-6, 2, 4]) { - t.true(is.evenInteger(element)); - t.notThrows(() => { - assert.evenInteger(element); + assert.ok(is.evenInteger(element)); + assert.doesNotThrow(() => { + isAssert.evenInteger(element); }); } for (const element of [-3, 1, 5]) { - t.false(is.evenInteger(element)); - t.throws(() => { - assert.evenInteger(element); + assert.strictEqual(is.evenInteger(element), false); + assert.throws(() => { + isAssert.evenInteger(element); }); } }); -test('is.oddInteger', t => { +test('is.oddInteger', () => { for (const element of [-5, 7, 13]) { - t.true(is.oddInteger(element)); - t.notThrows(() => { - assert.oddInteger(element); + assert.ok(is.oddInteger(element)); + assert.doesNotThrow(() => { + isAssert.oddInteger(element); }); } for (const element of [-8, 8, 10]) { - t.false(is.oddInteger(element)); - t.throws(() => { - assert.oddInteger(element); + assert.strictEqual(is.oddInteger(element), false); + assert.throws(() => { + isAssert.oddInteger(element); }); } }); -test('is.emptyArray', t => { - testType(t, 'emptyArray'); -}); +test('is.nonEmptyArray', () => { + assert.ok(is.nonEmptyArray([1, 2, 3])); + assert.strictEqual(is.nonEmptyArray([]), false); + assert.strictEqual(is.nonEmptyArray(new Array()), false); // eslint-disable-line @typescript-eslint/no-array-constructor -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]); + assert.doesNotThrow(() => { + isAssert.nonEmptyArray([1, 2, 3]); }); - t.throws(() => { - assert.nonEmptyArray([]); + assert.throws(() => { + isAssert.nonEmptyArray([]); }); - t.throws(() => { - assert.nonEmptyArray(new Array()); // eslint-disable-line @typescript-eslint/no-array-constructor + assert.throws(() => { + isAssert.nonEmptyArray(new Array()); // eslint-disable-line @typescript-eslint/no-array-constructor }); { @@ -1747,7 +1772,7 @@ test('is.nonEmptyArray', t => { const strings = ['🦄', 'unicorn'] as string[] | undefined; const function_ = (value: string) => value; - assert.nonEmptyArray(strings); + isAssert.nonEmptyArray(strings); const value = strings[0]; function_(value); @@ -1757,7 +1782,7 @@ test('is.nonEmptyArray', t => { const mixed = ['🦄', 'unicorn', 1, 2]; const function_ = (value: string | number) => value; - assert.nonEmptyArray(mixed); + isAssert.nonEmptyArray(mixed); const value = mixed[0]; function_(value); @@ -1767,35 +1792,33 @@ test('is.nonEmptyArray', t => { const arrays = [['🦄'], ['unicorn']]; const function_ = (value: string[]) => value; - assert.nonEmptyArray(arrays); + isAssert.nonEmptyArray(arrays); const value = arrays[0]; function_(value); } }); -test('is.emptyString', t => { - testType(t, 'emptyString', ['string']); - t.false(is.emptyString('🦄')); - t.throws(() => { - assert.emptyString('🦄'); +test('is.emptyString supplemental', () => { + assert.strictEqual(is.emptyString('🦄'), false); + assert.throws(() => { + isAssert.emptyString('🦄'); }); }); -test('is.emptyStringOrWhitespace', t => { - testType(t, 'emptyString', ['string']); - t.true(is.emptyStringOrWhitespace(' ')); - t.false(is.emptyStringOrWhitespace('🦄')); - t.false(is.emptyStringOrWhitespace('unicorn')); +test('is.emptyStringOrWhitespace supplemental', () => { + assert.ok(is.emptyStringOrWhitespace(' ')); + assert.strictEqual(is.emptyStringOrWhitespace('🦄'), false); + assert.strictEqual(is.emptyStringOrWhitespace('unicorn'), false); - t.notThrows(() => { - assert.emptyStringOrWhitespace(' '); + assert.doesNotThrow(() => { + isAssert.emptyStringOrWhitespace(' '); }); - t.throws(() => { - assert.emptyStringOrWhitespace('🦄'); + assert.throws(() => { + isAssert.emptyStringOrWhitespace('🦄'); }); - t.throws(() => { - assert.emptyStringOrWhitespace('unicorn'); + assert.throws(() => { + isAssert.emptyStringOrWhitespace('unicorn'); }); let value = 'test'; // eslint-disable-line prefer-const -- can't use `const` here because then it will be inferred as `never` in the `if` block @@ -1806,351 +1829,490 @@ test('is.emptyStringOrWhitespace', t => { } }); -test('is.nonEmptyString', t => { - t.false(is.nonEmptyString('')); - t.false(is.nonEmptyString(String())); - t.true(is.nonEmptyString('🦄')); +test('is.nonEmptyString', () => { + assert.strictEqual(is.nonEmptyString(''), false); + assert.strictEqual(is.nonEmptyString(String()), false); + assert.ok(is.nonEmptyString('🦄')); - t.throws(() => { - assert.nonEmptyString(''); + assert.throws(() => { + isAssert.nonEmptyString(''); }); - t.throws(() => { - assert.nonEmptyString(String()); + assert.throws(() => { + isAssert.nonEmptyString(String()); }); - t.notThrows(() => { - assert.nonEmptyString('🦄'); + assert.doesNotThrow(() => { + isAssert.nonEmptyString('🦄'); }); }); -test('is.nonEmptyStringAndNotWhitespace', t => { - t.false(is.nonEmptyStringAndNotWhitespace(' ')); - t.true(is.nonEmptyStringAndNotWhitespace('🦄')); +test('is.nonEmptyStringAndNotWhitespace', () => { + assert.strictEqual(is.nonEmptyStringAndNotWhitespace(' '), false); + assert.ok(is.nonEmptyStringAndNotWhitespace('🦄')); for (const value of [null, undefined, 5, Number.NaN, {}, []]) { - t.false(is.nonEmptyStringAndNotWhitespace(value)); + assert.strictEqual(is.nonEmptyStringAndNotWhitespace(value), false); - t.throws(() => { - assert.nonEmptyStringAndNotWhitespace(value); + assert.throws(() => { + isAssert.nonEmptyStringAndNotWhitespace(value); }); } - t.throws(() => { - assert.nonEmptyStringAndNotWhitespace(''); + assert.throws(() => { + isAssert.nonEmptyStringAndNotWhitespace(''); }); - t.notThrows(() => { - assert.nonEmptyStringAndNotWhitespace('🦄'); + assert.doesNotThrow(() => { + isAssert.nonEmptyStringAndNotWhitespace('🦄'); }); }); -test('is.emptyObject', t => { - t.true(is.emptyObject({})); - t.true(is.emptyObject(new Object())); // eslint-disable-line no-object-constructor - t.false(is.emptyObject({unicorn: '🦄'})); +test('is.emptyObject', () => { + assert.ok(is.emptyObject({})); + assert.ok(is.emptyObject(new Object())); // eslint-disable-line no-object-constructor + assert.strictEqual(is.emptyObject({unicorn: '🦄'}), false); + assert.strictEqual(is.emptyObject(function () {}), false); // eslint-disable-line prefer-arrow-callback + assert.strictEqual(is.emptyObject(() => {}), false); + assert.strictEqual(is.emptyObject(class Foo {}), false); // eslint-disable-line @typescript-eslint/no-extraneous-class + assert.strictEqual(is.emptyObject([]), false); + assert.strictEqual(is.emptyObject(['unicorn']), false); - t.notThrows(() => { - assert.emptyObject({}); + assert.doesNotThrow(() => { + isAssert.emptyObject({}); }); - t.notThrows(() => { - assert.emptyObject(new Object()); // eslint-disable-line no-object-constructor + assert.doesNotThrow(() => { + isAssert.emptyObject(new Object()); // eslint-disable-line no-object-constructor }); - t.throws(() => { - assert.emptyObject({unicorn: '🦄'}); + assert.throws(() => { + isAssert.emptyObject({unicorn: '🦄'}); + }); + assert.throws(() => { + isAssert.emptyObject(function () {}); // eslint-disable-line prefer-arrow-callback }); }); -test('is.nonEmptyObject', t => { +test('is.nonEmptyObject', () => { const foo = {}; is.nonEmptyObject(foo); - t.false(is.nonEmptyObject({})); - t.false(is.nonEmptyObject(new Object())); // eslint-disable-line no-object-constructor - t.true(is.nonEmptyObject({unicorn: '🦄'})); + assert.strictEqual(is.nonEmptyObject({}), false); + assert.strictEqual(is.nonEmptyObject(new Object()), false); // eslint-disable-line no-object-constructor + assert.ok(is.nonEmptyObject({unicorn: '🦄'})); - t.throws(() => { - assert.nonEmptyObject({}); + assert.strictEqual(is.nonEmptyObject([]), false); + assert.strictEqual(is.nonEmptyObject(['unicorn']), false); + + const functionWithProperty = function () {}; + (functionWithProperty as any).custom = 'value'; + assert.strictEqual(is.nonEmptyObject(functionWithProperty), false); + + assert.throws(() => { + isAssert.nonEmptyObject({}); }); - t.throws(() => { - assert.nonEmptyObject(new Object()); // eslint-disable-line no-object-constructor + assert.throws(() => { + isAssert.nonEmptyObject(new Object()); // eslint-disable-line no-object-constructor }); - t.notThrows(() => { - assert.nonEmptyObject({unicorn: '🦄'}); + assert.doesNotThrow(() => { + isAssert.nonEmptyObject({unicorn: '🦄'}); }); }); -test('is.emptySet', t => { - testType(t, 'emptySet'); -}); - -test('is.nonEmptySet', t => { +test('is.nonEmptySet', () => { const temporarySet = new Set(); - t.false(is.nonEmptySet(temporarySet)); - t.throws(() => { - assert.nonEmptySet(temporarySet); + assert.strictEqual(is.nonEmptySet(temporarySet), false); + assert.throws(() => { + isAssert.nonEmptySet(temporarySet); }); temporarySet.add(1); - t.true(is.nonEmptySet(temporarySet)); - t.notThrows(() => { - assert.nonEmptySet(temporarySet); + assert.ok(is.nonEmptySet(temporarySet)); + assert.doesNotThrow(() => { + isAssert.nonEmptySet(temporarySet); }); }); -test('is.emptyMap', t => { - testType(t, 'emptyMap'); -}); - -test('is.nonEmptyMap', t => { +test('is.nonEmptyMap', () => { const temporaryMap = new Map(); - t.false(is.nonEmptyMap(temporaryMap)); - t.throws(() => { - assert.nonEmptyMap(temporaryMap); + assert.strictEqual(is.nonEmptyMap(temporaryMap), false); + assert.throws(() => { + isAssert.nonEmptyMap(temporaryMap); }); temporaryMap.set('unicorn', '🦄'); - t.true(is.nonEmptyMap(temporaryMap)); - t.notThrows(() => { - assert.nonEmptyMap(temporaryMap); + assert.ok(is.nonEmptyMap(temporaryMap)); + assert.doesNotThrow(() => { + isAssert.nonEmptyMap(temporaryMap); }); }); -test('is.propertyKey', t => { - t.true(is.propertyKey('key')); - t.true(is.propertyKey(42)); - t.true(is.propertyKey(Symbol(''))); +test('is.propertyKey', () => { + assert.ok(is.propertyKey('key')); + assert.ok(is.propertyKey(42)); + assert.ok(is.propertyKey(Symbol(''))); - t.false(is.propertyKey(null)); - t.false(is.propertyKey(undefined)); - t.false(is.propertyKey(true)); - t.false(is.propertyKey({})); - t.false(is.propertyKey([])); - t.false(is.propertyKey(new Map())); - t.false(is.propertyKey(new Set())); + assert.strictEqual(is.propertyKey(null), false); + assert.strictEqual(is.propertyKey(undefined), false); + assert.strictEqual(is.propertyKey(true), false); + assert.strictEqual(is.propertyKey({}), false); + assert.strictEqual(is.propertyKey([]), false); + assert.strictEqual(is.propertyKey(new Map()), false); + assert.strictEqual(is.propertyKey(new Set()), false); + + // AssertPropertyKey should narrow to PropertyKey (string | number | symbol), not just number + const symbolValue: unknown = Symbol('test'); + assertPropertyKey(symbolValue); + expectTypeOf(symbolValue).toEqualTypeOf(); }); -test('is.any', t => { - t.true(is.any(is.string, {}, true, '🦄')); - t.true(is.any(is.object, false, {}, 'unicorns')); - t.false(is.any(is.boolean, '🦄', [], 3)); - t.false(is.any(is.integer, true, 'lol', {})); - t.true(is.any([is.string, is.number], {}, true, '🦄')); - t.false(is.any([is.boolean, is.number], 'unicorns', [], new Map())); +test('is.any', () => { + assert.ok(is.any(is.string, {}, true, '🦄')); + assert.ok(is.any(is.object, false, {}, 'unicorns')); + assert.strictEqual(is.any(is.boolean, '🦄', [], 3), false); + assert.strictEqual(is.any(is.integer, true, 'lol', {}), false); + assert.ok(is.any([is.string, is.number], {}, true, '🦄')); + assert.strictEqual(is.any([is.boolean, is.number], 'unicorns', [], new Map()), false); + assert.strictEqual(typeof is.any([is.string, is.number]), 'function'); - t.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + assert.throws(() => { is.any(null as any, true); }); - t.throws(() => { + assert.throws(() => { + is.any([], 'value'); + }); + + assert.throws(() => { is.any(is.string); }); - t.notThrows(() => { - assert.any(is.string, {}, true, '🦄'); + assert.doesNotThrow(() => { + isAssert.any(is.string, {}, true, '🦄'); }); - t.notThrows(() => { - assert.any(is.object, false, {}, 'unicorns'); + assert.doesNotThrow(() => { + isAssert.any(is.object, false, {}, 'unicorns'); }); - t.throws(() => { - assert.any(is.boolean, '🦄', [], 3); + assert.throws(() => { + isAssert.any([is.string, is.number]); }); - t.throws(() => { - assert.any(is.integer, true, 'lol', {}); + assert.throws(() => { + isAssert.any(is.boolean, '🦄', [], 3); }); - t.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - assert.any(null as any, true); + assert.throws(() => { + isAssert.any(is.integer, true, 'lol', {}); }); - t.throws(() => { - assert.any(is.string); + assert.throws(() => { + isAssert.any(null as any, true); }); - t.throws(() => { - assert.any(is.string, 1, 2, 3); + assert.throws(() => { + isAssert.any([], 'value'); + }); + + assert.throws(() => { + isAssert.any(is.string); + }); + + assert.throws(() => { + isAssert.any(is.string, 1, 2, 3); }, { // Includes expected type and removes duplicates from received types: - message: /Expected values which are `string`. Received values of type `number`./, + // eslint-disable-next-line prefer-regex-literals + message: new RegExp('Expected values which are `string`. Received values of type `number`.', 'v'), }); - t.throws(() => { - assert.any(is.string, 1, [4]); + assert.throws(() => { + isAssert.any(is.string, 1, [4]); }, { // Includes expected type and lists all received types: - message: /Expected values which are `string`. Received values of types `number` and `Array`./, + // eslint-disable-next-line prefer-regex-literals + message: new RegExp('Expected values which are `string`. Received values of types `number` and `Array`.', 'v'), }); - t.throws(() => { - assert.any([is.string, is.nullOrUndefined], 1); + assert.throws(() => { + isAssert.any([is.string, is.nullOrUndefined], 1); }, { // Handles array as first argument: - message: /Expected values which are `string` or `null or undefined`. Received values of type `number`./, + // eslint-disable-next-line prefer-regex-literals + message: new RegExp('Expected values which are `string` or `null or undefined`. Received values of type `number`.', 'v'), }); - t.throws(() => { - assert.any([is.string, is.number, is.boolean], null, undefined, Number.NaN); + assert.throws(() => { + isAssert.any([is.string, is.number, is.boolean], null, undefined, Number.NaN); }, { // Handles more than 2 expected and received types: - message: /Expected values which are `string`, `number`, or `boolean`. Received values of types `null`, `undefined`, and `NaN`./, + // eslint-disable-next-line prefer-regex-literals + message: new RegExp('Expected values which are `string`, `number`, or `boolean`. Received values of types `null`, `undefined`, and `NaN`.', 'v'), }); - t.throws(() => { - assert.any(() => false, 1); + assert.throws(() => { + isAssert.any(() => false, 1); }, { // Default type assertion message - message: /Expected values which are `predicate returns truthy for any value`./, + // eslint-disable-next-line prefer-regex-literals + message: new RegExp('Expected values which are `predicate returns truthy for any value`.', 'v'), }); }); -test('is.all', t => { - t.true(is.all(is.object, {}, new Set(), new Map())); - t.true(is.all(is.boolean, true, false)); - t.false(is.all(is.string, '🦄', [])); - t.false(is.all(is.set, new Map(), {})); +test('is.all', () => { + assert.ok(is.all(is.object, {}, new Set(), new Map())); + assert.ok(is.all(is.boolean, true, false)); + assert.strictEqual(is.all(is.string, '🦄', []), false); + assert.strictEqual(is.all(is.set, new Map(), {}), false); - t.true(is.all(is.array, ['1'], ['2'])); + assert.ok(is.all(is.array, ['1'], ['2'])); + assert.ok(is.all([is.string, is.nonEmptyString], '🦄', 'unicorns')); + assert.strictEqual(is.all([is.string, is.number], '🦄'), false); - t.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument + assert.throws(() => { is.all(null as any, true); }); - t.throws(() => { + assert.throws(() => { + is.all([], 'value'); + }); + + assert.throws(() => { is.all(is.string); }); - t.notThrows(() => { - assert.all(is.object, {}, new Set(), new Map()); + assert.doesNotThrow(() => { + isAssert.all(is.object, {}, new Set(), new Map()); }); - t.notThrows(() => { - assert.all(is.boolean, true, false); + assert.doesNotThrow(() => { + isAssert.all(is.boolean, true, false); }); - t.throws(() => { - assert.all(is.string, '🦄', []); + assert.throws(() => { + isAssert.all([is.string, is.number]); }); - t.throws(() => { - assert.all(is.set, new Map(), {}); + assert.doesNotThrow(() => { + isAssert.all([is.string, is.nonEmptyString], '🦄', 'unicorns'); }); - t.throws(() => { - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - assert.all(null as any, true); + assert.throws(() => { + isAssert.all(is.string, '🦄', []); }); - t.throws(() => { - assert.all(is.string); + assert.throws(() => { + isAssert.all([is.string, is.number], '🦄'); }); - t.throws(() => { - assert.all(is.string, 1, 2, 3); + assert.throws(() => { + isAssert.all(is.set, new Map(), {}); + }); + + assert.throws(() => { + isAssert.all(null as any, true); + }); + + assert.throws(() => { + isAssert.all([], 'value'); + }); + + assert.throws(() => { + isAssert.all(is.string); + }); + + assert.throws(() => { + isAssert.all(is.string, 1, 2, 3); }, { // Includes expected type and removes duplicates from received types: - message: /Expected values which are `string`. Received values of type `number`./, + // eslint-disable-next-line prefer-regex-literals + message: new RegExp('Expected values which are `string`. Received values of type `number`.', 'v'), }); - t.throws(() => { - assert.all(is.string, 1, [4]); + assert.throws(() => { + isAssert.all(is.string, 1, [4]); }, { // Includes expected type and lists all received types: - message: /Expected values which are `string`. Received values of types `number` and `Array`./, + // eslint-disable-next-line prefer-regex-literals + message: new RegExp('Expected values which are `string`. Received values of types `number` and `Array`.', 'v'), }); - t.throws(() => { - assert.all(() => false, 1); + assert.throws(() => { + isAssert.all(() => false, 1); }, { // Default type assertion message - message: /Expected values which are `predicate returns truthy for all values`./, + // eslint-disable-next-line prefer-regex-literals + message: new RegExp('Expected values which are `predicate returns truthy for all values`.', 'v'), }); }); -test('is.formData', t => { +test('is.any as predicate factory', () => { + // Returns a type guard function when called with only predicates + const isStringOrNumber = is.any([is.string, is.number]); + assert.strictEqual(typeof isStringOrNumber, 'function'); + assert.ok(isStringOrNumber('hello')); + assert.ok(isStringOrNumber(123)); + assert.strictEqual(isStringOrNumber(true), false); + assert.strictEqual(isStringOrNumber({}), false); + + // Type narrowing works correctly (compile-time check) + const value: unknown = 'test'; + if (isStringOrNumber(value)) { + // TypeScript should narrow to string | number + const narrowed: string | number = value; + assert.ok(typeof narrowed === 'string' || typeof narrowed === 'number'); + } + + // Works with is.optional + assert.ok(is.optional(undefined, is.any([is.string, is.number]))); + assert.ok(is.optional('test', is.any([is.string, is.number]))); + assert.ok(is.optional(42, is.any([is.string, is.number]))); + assert.strictEqual(is.optional(true, is.any([is.string, is.number])), false); + + const predicateArray: Predicate[] = [is.string, is.number]; + const isStringOrNumberFromArray = is.any(predicateArray); + assert.strictEqual(typeof isStringOrNumberFromArray, 'function'); + assert.ok(isStringOrNumberFromArray('hello')); + assert.ok(isStringOrNumberFromArray(123)); + assert.strictEqual(isStringOrNumberFromArray(true), false); + + // Type narrowing with is.optional (compile-time check) + const optionalValue: unknown = undefined; + if (is.optional(optionalValue, is.any([is.string, is.number]))) { + // TypeScript should narrow to string | number | undefined + const narrowed: string | number | undefined = optionalValue; + assert.ok(narrowed === undefined || typeof narrowed === 'string' || typeof narrowed === 'number'); + } + + // Works with more predicates + const isStringOrNumberOrBoolean = is.any([is.string, is.number, is.boolean]); + assert.ok(isStringOrNumberOrBoolean('hello')); + assert.ok(isStringOrNumberOrBoolean(123)); + assert.ok(isStringOrNumberOrBoolean(true)); + assert.strictEqual(isStringOrNumberOrBoolean({}), false); + + assert.throws(() => { + is.any([is.string, 123 as any]); + }); +}); + +test('is.all as predicate factory', () => { + // Returns a type guard function when called with only predicates + const isArrayAndNonEmpty = is.all([is.array, is.nonEmptyArray]); + assert.strictEqual(typeof isArrayAndNonEmpty, 'function'); + assert.ok(isArrayAndNonEmpty(['hello'])); + assert.strictEqual(isArrayAndNonEmpty([]), false); + assert.strictEqual(isArrayAndNonEmpty('hello'), false); + + // Type narrowing works correctly + const value: unknown = ['test']; + if (isArrayAndNonEmpty(value)) { + // TypeScript should narrow to the intersection type + assert.ok(Array.isArray(value)); + assert.ok(value.length > 0); + } + + // Works with is.optional + assert.ok(is.optional(undefined, is.all([is.object, is.plainObject]))); + assert.ok(is.optional({foo: 'bar'}, is.all([is.object, is.plainObject]))); + assert.strictEqual(is.optional([], is.all([is.object, is.plainObject])), false); + + assert.throws(() => { + is.all([is.string, 123 as any]); + }); +}); + +test('is.formData supplemental', () => { const data = new window.FormData(); - t.true(is.formData(data)); - t.false(is.formData({})); - t.false(is.formData(undefined)); - t.false(is.formData(null)); + assert.ok(is.formData(data)); + assert.strictEqual(is.formData({}), false); + assert.strictEqual(is.formData(undefined), false); + assert.strictEqual(is.formData(null), false); - t.notThrows(() => { - assert.formData(data); + assert.doesNotThrow(() => { + isAssert.formData(data); }); - t.throws(() => { - assert.formData({}); + assert.throws(() => { + isAssert.formData({}); }); - t.throws(() => { - assert.formData(undefined); + assert.throws(() => { + isAssert.formData(undefined); }); - t.throws(() => { - assert.formData(null); + assert.throws(() => { + isAssert.formData(null); }); }); -test('is.urlSearchParams', t => { +test('is.urlSearchParams', () => { const searchParameters = new URLSearchParams(); - t.true(is.urlSearchParams(searchParameters)); - t.false(is.urlSearchParams({})); - t.false(is.urlSearchParams(undefined)); - t.false(is.urlSearchParams(null)); + assert.ok(is.urlSearchParams(searchParameters)); + assert.strictEqual(is.urlSearchParams({}), false); + assert.strictEqual(is.urlSearchParams(undefined), false); + assert.strictEqual(is.urlSearchParams(null), false); - t.notThrows(() => { - assert.urlSearchParams(searchParameters); + assert.doesNotThrow(() => { + isAssert.urlSearchParams(searchParameters); }); - t.throws(() => { - assert.urlSearchParams({}); + assert.throws(() => { + isAssert.urlSearchParams({}); }); - t.throws(() => { - assert.urlSearchParams(undefined); + assert.throws(() => { + isAssert.urlSearchParams(undefined); }); - t.throws(() => { - assert.urlSearchParams(null); + assert.throws(() => { + isAssert.urlSearchParams(null); }); }); -test('is.validDate', t => { - t.true(is.validDate(new Date())); - t.false(is.validDate(new Date('x'))); - t.notThrows(() => { - assert.validDate(new Date()); +test('is.validDate', () => { + assert.ok(is.validDate(new Date())); + assert.strictEqual(is.validDate(new Date('x')), false); + assert.doesNotThrow(() => { + isAssert.validDate(new Date()); }); - t.throws(() => { - assert.validDate(new Date('x')); + assert.throws(() => { + isAssert.validDate(new Date('x')); }); }); -test('is.validLength', t => { - t.true(is.validLength(1)); - t.true(is.validLength(0)); - t.false(is.validLength(-1)); - t.false(is.validLength(0.1)); - t.notThrows(() => { - assert.validLength(1); +test('is.validLength', () => { + assert.ok(is.validLength(1)); + assert.ok(is.validLength(0)); + assert.strictEqual(is.validLength(-1), false); + assert.strictEqual(is.validLength(0.1), false); + assert.doesNotThrow(() => { + isAssert.validLength(1); }); - t.throws(() => { - assert.validLength(-1); + assert.throws(() => { + isAssert.validLength(-1); + }); + assert.throws(() => { + isAssert.validLength(0.1); }); }); -test('is.whitespaceString', t => { - t.true(is.whitespaceString(' ')); - t.true(is.whitespaceString(' ')); - t.true(is.whitespaceString('   ')); - t.true(is.whitespaceString('\u3000')); - t.true(is.whitespaceString(' ')); - t.false(is.whitespaceString('')); - t.false(is.whitespaceString('-')); - t.false(is.whitespaceString(' hi ')); +test('is.whitespaceString', () => { + assert.ok(is.whitespaceString(' ')); + assert.ok(is.whitespaceString(' ')); + assert.ok(is.whitespaceString('   ')); + assert.ok(is.whitespaceString('\u3000')); + assert.ok(is.whitespaceString(' ')); + assert.strictEqual(is.whitespaceString(''), false); + assert.strictEqual(is.whitespaceString('-'), false); + assert.strictEqual(is.whitespaceString(' hi '), false); + + assert.doesNotThrow(() => { + isAssert.whitespaceString(' '); + }); + assert.throws(() => { + isAssert.whitespaceString(''); + }); + assert.throws(() => { + isAssert.whitespaceString(' hi '); + }); }); -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); +test('assert', () => { + // Contrived test showing that TypeScript acknowledges the type assertion in `isAssert.number()`. + // Real-world usage includes asserting user input, but here we use a random number/string generator. const getNumberOrStringRandomly = (): number | string => { const random = Math.random(); @@ -2164,7 +2326,7 @@ test('assert', t => { const canUseOnlyNumber = (badlyTypedArgument: any): number => { // Narrow the type to number, or throw an error at runtime for non-numbers. - assert.number(badlyTypedArgument); + isAssert.number(badlyTypedArgument); // Both the type and runtime value is number. return 1000 * badlyTypedArgument; @@ -2172,373 +2334,489 @@ test('assert', t => { const badlyTypedVariable: any = getNumberOrStringRandomly(); - t.true(is.number(badlyTypedVariable) || is.string(badlyTypedVariable)); + assert.ok(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)); + assert.ok(is.number(result)); } catch { // Assertion was tripped. - t.true(is.string(badlyTypedVariable)); + assert.ok(is.string(badlyTypedVariable)); } }); -test('custom assertion message', t => { +test('custom assertion message', () => { const message = 'Custom error message'; - t.throws(() => { - assert.array(undefined, undefined, message); - }, {instanceOf: TypeError, message}); + const assertThrowsTypeErrorWithMessage = (assertion: () => void) => { + // `node:assert` does not verify the error class when matching only on `{message}`. + assert.throws(assertion, error => { + assert.ok(error instanceof TypeError); + assert.strictEqual(error.message, message); + return true; + }); + }; - t.throws(() => { - assert.arrayBuffer(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.array(undefined, undefined, message); + }); - t.throws(() => { - assert.arrayLike(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.arrayBuffer(undefined, message); + }); - t.throws(() => { - assert.asyncFunction(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.arrayLike(undefined, message); + }); - t.throws(() => { - assert.asyncGenerator(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.asyncFunction(undefined, message); + }); - t.throws(() => { - assert.asyncGeneratorFunction(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.asyncGenerator(undefined, message); + }); - t.throws(() => { - assert.asyncIterable(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.asyncGeneratorFunction(undefined, message); + }); - t.throws(() => { - assert.bigInt64Array(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.asyncIterable(undefined, message); + }); - t.throws(() => { - assert.bigUint64Array(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.bigInt64Array(undefined, message); + }); - t.throws(() => { - assert.bigint(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.bigUint64Array(undefined, message); + }); - t.throws(() => { - assert.blob(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.bigint(undefined, message); + }); - t.throws(() => { - assert.boolean(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.blob(undefined, message); + }); - t.throws(() => { - assert.boundFunction(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.boolean(undefined, message); + }); - t.throws(() => { - assert.buffer(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.boundFunction(undefined, message); + }); - t.throws(() => { - assert.class(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.buffer(undefined, message); + }); - t.throws(() => { - assert.dataView(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.class(undefined, message); + }); - t.throws(() => { - assert.date(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.dataView(undefined, message); + }); - t.throws(() => { - assert.directInstanceOf(undefined, Error, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.date(undefined, message); + }); - t.throws(() => { - assert.emptyArray(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.directInstanceOf(undefined, Error, message); + }); - t.throws(() => { - assert.emptyMap(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.emptyArray(undefined, message); + }); - t.throws(() => { - assert.emptyObject(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.emptyMap(undefined, message); + }); - t.throws(() => { - assert.emptySet(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.emptyObject(undefined, message); + }); - t.throws(() => { - assert.emptyString(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.emptySet(undefined, message); + }); - t.throws(() => { - assert.emptyStringOrWhitespace(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.emptyString(undefined, message); + }); - t.throws(() => { + assertThrowsTypeErrorWithMessage(() => { + isAssert.emptyStringOrWhitespace(undefined, message); + }); + + assertThrowsTypeErrorWithMessage(() => { enum Enum {} - assert.enumCase('invalid', Enum, message); - }, {instanceOf: TypeError, message}); + isAssert.enumCase('invalid', Enum, message); + }); - t.throws(() => { - assert.error(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.error(undefined, message); + }); - t.throws(() => { - assert.evenInteger(33, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.evenInteger(33, message); + }); - t.throws(() => { - assert.falsy(true, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.falsy(true, message); + }); - t.throws(() => { - assert.float32Array(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.finiteNumber(Number.POSITIVE_INFINITY, message); + }); - t.throws(() => { - assert.float64Array(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.float32Array(undefined, message); + }); - t.throws(() => { - assert.formData(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.float64Array(undefined, message); + }); - t.throws(() => { - assert.function(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.formData(undefined, message); + }); - t.throws(() => { - assert.generator(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.function(undefined, message); + }); - t.throws(() => { - assert.generatorFunction(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.generator(undefined, message); + }); - t.throws(() => { - assert.htmlElement(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.generatorFunction(undefined, message); + }); - t.throws(() => { - assert.inRange(5, [1, 2], message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.htmlElement(undefined, message); + }); - t.throws(() => { - assert.infinite(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.inRange(5, [1, 2], message); + }); - t.throws(() => { - assert.int16Array(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.infinite(undefined, message); + }); - t.throws(() => { - assert.int32Array(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.int16Array(undefined, message); + }); - t.throws(() => { - assert.int8Array(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.int32Array(undefined, message); + }); - t.throws(() => { - assert.integer(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.int8Array(undefined, message); + }); - t.throws(() => { - assert.iterable(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.integer(undefined, message); + }); - t.throws(() => { - assert.map(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.iterable(undefined, message); + }); - t.throws(() => { - assert.nan(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.map(undefined, message); + }); - t.throws(() => { - assert.nativePromise(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.nan(undefined, message); + }); - t.throws(() => { - assert.negativeNumber(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.nativePromise(undefined, message); + }); - t.throws(() => { - assert.nodeStream(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.not.undefined(undefined, message); + }); - t.throws(() => { - assert.nonEmptyArray(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.not.string('hello', message); + }); - t.throws(() => { - assert.nonEmptyMap(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.negativeNumber(undefined, message); + }); - t.throws(() => { - assert.nonEmptyObject(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.nodeStream(undefined, message); + }); - t.throws(() => { - assert.nonEmptySet(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.nonEmptyArray(undefined, message); + }); - t.throws(() => { - assert.nonEmptyString(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.nonEmptyMap(undefined, message); + }); - t.throws(() => { - assert.nonEmptyStringAndNotWhitespace(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.nonEmptyObject(undefined, message); + }); - t.throws(() => { - assert.null(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.nonEmptySet(undefined, message); + }); - t.throws(() => { - assert.nullOrUndefined(false, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.nonEmptyString(undefined, message); + }); - t.throws(() => { - assert.number(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.nonEmptyStringAndNotWhitespace(undefined, message); + }); - t.throws(() => { - assert.numericString(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.nonNegativeNumber(-1, message); + }); - t.throws(() => { - assert.object(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.null(undefined, message); + }); - t.throws(() => { - assert.observable(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.nullOrUndefined(false, message); + }); - t.throws(() => { - assert.oddInteger(42, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.number(undefined, message); + }); - t.throws(() => { - assert.plainObject(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.numericString(undefined, message); + }); - t.throws(() => { - assert.positiveNumber(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.object(undefined, message); + }); - t.throws(() => { - assert.primitive([], message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.observable(undefined, message); + }); - t.throws(() => { - assert.promise(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.oddInteger(42, message); + }); - t.throws(() => { - assert.propertyKey(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.plainObject(undefined, message); + }); - t.throws(() => { - assert.regExp(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.positiveInteger(0, message); + }); - t.throws(() => { - assert.safeInteger(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.positiveNumber(undefined, message); + }); - t.throws(() => { - assert.set(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.primitive([], message); + }); - t.throws(() => { - assert.sharedArrayBuffer(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.promise(undefined, message); + }); - t.throws(() => { - assert.string(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.propertyKey(undefined, message); + }); - t.throws(() => { - assert.symbol(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.regExp(undefined, message); + }); - t.throws(() => { - assert.truthy(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.safeInteger(undefined, message); + }); - t.throws(() => { - assert.tupleLike(undefined, [], message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.set(undefined, message); + }); - t.throws(() => { - assert.typedArray(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.sharedArrayBuffer(undefined, message); + }); - t.throws(() => { - assert.uint16Array(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.string(undefined, message); + }); - t.throws(() => { - assert.uint32Array(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.symbol(undefined, message); + }); - t.throws(() => { - assert.uint8Array(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.truthy(undefined, message); + }); - t.throws(() => { - assert.uint8ClampedArray(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.tupleLike(undefined, [], message); + }); - t.throws(() => { - assert.undefined(false, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.typedArray(undefined, message); + }); - t.throws(() => { - assert.urlInstance(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.uint16Array(undefined, message); + }); - t.throws(() => { - assert.urlSearchParams(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.uint32Array(undefined, message); + }); - t.throws(() => { - assert.urlString(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.uint8Array(undefined, message); + }); - t.throws(() => { - assert.validDate(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.uint8ClampedArray(undefined, message); + }); - t.throws(() => { - assert.validLength(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.undefined(false, message); + }); - t.throws(() => { - assert.weakMap(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.urlInstance(undefined, message); + }); - t.throws(() => { - assert.weakRef(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.urlSearchParams(undefined, message); + }); - t.throws(() => { - assert.weakSet(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.urlString(undefined, message); + }); - t.throws(() => { - assert.whitespaceString(undefined, message); - }, {instanceOf: TypeError, message}); + assertThrowsTypeErrorWithMessage(() => { + isAssert.validDate(undefined, message); + }); + + assertThrowsTypeErrorWithMessage(() => { + isAssert.validLength(undefined, message); + }); + + assertThrowsTypeErrorWithMessage(() => { + isAssert.weakMap(undefined, message); + }); + + assertThrowsTypeErrorWithMessage(() => { + isAssert.weakRef(undefined, message); + }); + + assertThrowsTypeErrorWithMessage(() => { + isAssert.weakSet(undefined, message); + }); + + assertThrowsTypeErrorWithMessage(() => { + isAssert.whitespaceString(undefined, message); + }); +}); + +test('isAssert.not.undefined', () => { + assert.throws(() => { + isAssert.not.undefined(undefined); + }, { + message: 'Expected value which is not `undefined`, received value of type `undefined`.', + }); + + assert.doesNotThrow(() => { + isAssert.not.undefined(null); + }); + + assert.doesNotThrow(() => { + isAssert.not.undefined(false); + }); + + assert.doesNotThrow(() => { + isAssert.not.undefined(0); + }); + + assert.doesNotThrow(() => { + isAssert.not.undefined(''); + }); +}); + +test('isAssert.not', () => { + assert.deepStrictEqual(new Set(keysOf(isAssert.not)), new Set(keysOf(notAssertionFixtures))); + + for (const type of keysOf(notAssertionFixtures)) { + const {nonFixture, typeDescription} = notAssertionFixtures[type]; + const testAssert = isAssert.not[type]; + const fixtures = 'fixtures' in notAssertionFixtures[type] ? notAssertionFixtures[type].fixtures : [notAssertionFixtures[type].fixture]; + + for (const fixture of fixtures) { + assert.throws(() => { + testAssert(fixture); + }, { + message: `Expected value which is not \`${typeDescription}\`, received value of type \`${is(fixture)}\`.`, + }); + } + + assert.doesNotThrow(() => { + testAssert(nonFixture); + }); + } + + assert.strictEqual('number' in isAssert.not, false); + assert.strictEqual('integer' in isAssert.not, false); + assert.strictEqual('object' in isAssert.not, false); + assert.strictEqual('blob' in isAssert.not, false); + assert.strictEqual('array' in isAssert.not, false); + assert.strictEqual('date' in isAssert.not, false); + assert.strictEqual('function' in isAssert.not, false); + assert.strictEqual('map' in isAssert.not, false); + assert.strictEqual('set' in isAssert.not, false); +}); + +test('isAssert.not edge cases', () => { + assert.doesNotThrow(() => { + isAssert.not.null(undefined); + }); +}); + +test('is.optional', () => { + assert.ok(is.optional(undefined, is.string)); + assert.ok(is.optional('🦄', is.string)); + assert.strictEqual(is.optional(123, is.string), false); + assert.strictEqual(is.optional(null, is.string), false); +}); + +test('isAssert.optional', () => { + assert.doesNotThrow(() => { + isAssert.optional(undefined, isAssert.string); + }); + + assert.doesNotThrow(() => { + isAssert.optional('🦄', isAssert.string); + }); + + assert.throws(() => { + isAssert.optional(123, isAssert.string); + }); + + assert.throws(() => { + isAssert.optional(null, isAssert.string); + }); }); diff --git a/test/tsconfig.json b/test/tsconfig.json new file mode 100644 index 0000000..1f492d3 --- /dev/null +++ b/test/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "rootDir": "..", + "noUnusedLocals": false, + "noUnusedParameters": false + }, + "include": [ + "../source", + "type-tests.ts" + ] +} diff --git a/test/type-tests.ts b/test/type-tests.ts new file mode 100644 index 0000000..f08fa25 --- /dev/null +++ b/test/type-tests.ts @@ -0,0 +1,405 @@ +import {expectTypeOf} from 'expect-type'; +import is, { + assert as isAssert, + assertNotNullOrUndefined, + assertNotPrimitive, + assertNotString, + assertNotUndefined, + type EvenInteger, + type FiniteNumber, + type Integer, + type NaN as NaNType, + type NegativeInfinity, + type NegativeInteger, + type NegativeNumber, + type NonNegativeInteger, + type NonNegativeNumber, + type OddInteger, + type PositiveInfinity, + type PositiveInteger, + type PositiveNumber, + type Primitive, + type SafeInteger, + type ValidLength, +} from '../source/index.ts'; + +// eslint-disable-next-line @typescript-eslint/no-restricted-types +type UnknownNotPrimitive = Exclude | object; + +// For each predicate, verify two things: +// 1. True branch narrows to the branded type. +// 2. False branch on a `number` input stays `number` (not `never`). +// Without the branded types, `Exclude` = `never` would break +// the common validation-guard pattern: if (!is.X(n)) throw; use(n). + +const nanCheck = (value: number) => { + if (is.nan(value)) { + const _: NaNType = value; + } else { + const _: number = value; + } +}; + +const finiteNumberCheck = (value: number) => { + if (is.finiteNumber(value)) { + const _: FiniteNumber = value; + } else { + const _: number = value; + } +}; + +const nonNegativeNumberCheck = (value: number) => { + if (is.nonNegativeNumber(value)) { + const _: NonNegativeNumber = value; + } else { + const _: number = value; + } +}; + +const positiveIntegerCheck = (value: number) => { + if (is.positiveInteger(value)) { + const _: PositiveInteger = value; + const __: Integer = value; + const ___: NonNegativeInteger = value; + } else { + const _: number = value; + } +}; + +const negativeIntegerCheck = (value: number) => { + if (is.negativeInteger(value)) { + const _: NegativeInteger = value; + const __: Integer = value; + } else { + const _: number = value; + } +}; + +const nonNegativeIntegerCheck = (value: number) => { + if (is.nonNegativeInteger(value)) { + const _: NonNegativeInteger = value; + const __: Integer = value; + } else { + const _: number = value; + } +}; + +const infiniteCheck = (value: number) => { + if (is.infinite(value)) { + const _: PositiveInfinity | NegativeInfinity = value; + const __: PositiveNumber | NegativeNumber = value; + } else { + const _: number = value; + } +}; + +const integerCheck = (value: number) => { + if (is.integer(value)) { + const _: Integer = value; + const __: FiniteNumber = value; + } else { + const _: number = value; + } +}; + +const safeIntegerCheck = (value: number) => { + if (is.safeInteger(value)) { + const _: SafeInteger = value; + const __: Integer = value; + } else { + const _: number = value; + } +}; + +const evenIntegerCheck = (value: number) => { + if (is.evenInteger(value)) { + const _: EvenInteger = value; + const __: Integer = value; + } else { + const _: number = value; + } +}; + +const oddIntegerCheck = (value: number) => { + if (is.oddInteger(value)) { + const _: OddInteger = value; + const __: Integer = value; + } else { + const _: number = value; + } +}; + +const positiveNumberCheck = (value: number) => { + if (is.positiveNumber(value)) { + const _: PositiveNumber = value; + const __: NonNegativeNumber = value; + } else { + const _: number = value; + } +}; + +const negativeNumberCheck = (value: number) => { + if (is.negativeNumber(value)) { + const _: NegativeNumber = value; + } else { + const _: number = value; + } +}; + +const validLengthCheck = (value: number) => { + if (is.validLength(value)) { + const _: ValidLength = value; + const __: SafeInteger = value; + const ___: NonNegativeInteger = value; + } else { + const _: number = value; + } +}; + +const integerUnknownCheck = (value: unknown) => { + if (is.integer(value)) { + const _: Integer = value; + const __: FiniteNumber = value; + } +}; + +const positiveIntegerUnknownCheck = (value: unknown) => { + if (is.positiveInteger(value)) { + const _: PositiveInteger = value; + const __: NonNegativeInteger = value; + } +}; + +const integerMixedUnionCheck = (value: string | number) => { + if (is.integer(value)) { + const _: number = value; + } else { + const _: string = value; + } +}; + +const positiveNumberMixedUnionCheck = (value: string | number) => { + if (is.positiveNumber(value)) { + const _: number = value; + } else { + const _: string = value; + } +}; + +const chainedNumericGuardCheck = (value: number) => { + if (is.positiveNumber(value) && is.integer(value)) { + const _: PositiveNumber = value; + const __: Integer = value; + const ___: FiniteNumber = value; + } +}; + +const distinctNumericBrandsStayDistinct = ( + positiveInteger: PositiveInteger, + negativeInteger: NegativeInteger, + validLength: ValidLength, +) => { + // @ts-expect-error -- Distinct numeric refinements must not collapse into each other. + const _: NegativeInteger = positiveInteger; + // @ts-expect-error -- ValidLength is non-negative and must not become a signed integer refinement. + const __: NegativeInteger = validLength; + + return negativeInteger; +}; + +const assertNotUndefinedCheck = (value: string | undefined) => { + isAssert.not.undefined(value); + expectTypeOf(value).toEqualTypeOf(); +}; + +const assertNotUndefinedUnknownCheck = (value: unknown) => { + isAssert.not.undefined(value); + expectTypeOf(value).toEqualTypeOf>(); +}; + +const assertNotUndefinedGenericCheck = (value: T) => { + isAssert.not.undefined(value); + const _: Exclude = value; +}; + +const nullValue = null; +type Null = typeof nullValue; + +const assertNotNullUnknownCheck = (value: unknown) => { + isAssert.not.null(value); + expectTypeOf(value).toEqualTypeOf>(); +}; + +const assertNotNullOrUndefinedCheck = (value: string | Null | undefined) => { + isAssert.not.nullOrUndefined(value); + expectTypeOf(value).toEqualTypeOf(); +}; + +const assertNotNullOrUndefinedUnknownCheck = (value: unknown) => { + isAssert.not.nullOrUndefined(value); + expectTypeOf(value).toEqualTypeOf>(); +}; + +const assertNotStringCheck = (value: string | number) => { + isAssert.not.string(value); + expectTypeOf(value).toEqualTypeOf(); +}; + +const assertNotStringUnknownCheck = (value: unknown) => { + isAssert.not.string(value); + expectTypeOf(value).toEqualTypeOf>(); +}; + +const assertNotStringGenericCheck = (value: T) => { + isAssert.not.string(value); + const _: Exclude = value; +}; + +const assertNotBooleanUnknownCheck = (value: unknown) => { + isAssert.not.boolean(value); + expectTypeOf(value).toEqualTypeOf>(); +}; + +const assertNotSymbolUnknownCheck = (value: unknown) => { + isAssert.not.symbol(value); + expectTypeOf(value).toEqualTypeOf>(); +}; + +const assertNotBigintUnknownCheck = (value: unknown) => { + isAssert.not.bigint(value); + expectTypeOf(value).toEqualTypeOf>(); +}; + +const assertNotPrimitiveUnknownCheck = (value: unknown) => { + isAssert.not.primitive(value); + // eslint-disable-next-line @typescript-eslint/no-restricted-types + expectTypeOf(value).toEqualTypeOf(); +}; + +const assertNotPrimitiveGenericCheck = (value: T) => { + isAssert.not.primitive(value); + const _: Exclude = value; +}; + +const assertNotNamedUndefinedExportCheck = (value: 0 | false | '' | Null | undefined | 'ok') => { + assertNotUndefined(value); + expectTypeOf(value).toEqualTypeOf<0 | false | '' | Null | 'ok'>(); +}; + +const assertNotNamedNullOrUndefinedUnknownExportCheck = (value: unknown) => { + assertNotNullOrUndefined(value); + expectTypeOf(value).toEqualTypeOf>(); +}; + +const assertNotNamedStringExportCheck = (value: string | number) => { + assertNotString(value); + expectTypeOf(value).toEqualTypeOf(); +}; + +const assertNotNamedStringUnknownExportCheck = (value: unknown) => { + assertNotString(value); + expectTypeOf(value).toEqualTypeOf>(); +}; + +const assertNotNamedPrimitiveUnknownExportCheck = (value: unknown) => { + assertNotPrimitive(value); + // eslint-disable-next-line @typescript-eslint/no-restricted-types + expectTypeOf(value).toEqualTypeOf(); +}; + +const assertNotCallableDoesNotExistCheck = (value: string | undefined) => { + // @ts-expect-error -- Generic negative assertions cannot safely infer complement types from arbitrary predicates. + isAssert.not(is.undefined, value); + const _: string | undefined = value; +}; + +const assertNotNumberDoesNotExistCheck = (value: string | number) => { + // @ts-expect-error -- `is.number` rejects `NaN`, so a narrowing negative assertion would be unsound. + isAssert.not.number(value); // eslint-disable-line @typescript-eslint/no-unsafe-call + const _: string | number = value; +}; + +const assertNotIntegerDoesNotExistCheck = (value: string | number) => { + // @ts-expect-error -- Numeric refinements are intentionally excluded from `assert.not`. + isAssert.not.integer(value); // eslint-disable-line @typescript-eslint/no-unsafe-call + const _: string | number = value; +}; + +const assertNotObjectDoesNotExistCheck = (value: Record | string) => { + // @ts-expect-error -- TypeScript's `{}` type includes primitives, so `not.object` cannot safely narrow every object-like input. + isAssert.not.object(value); // eslint-disable-line @typescript-eslint/no-unsafe-call + const _: Record | string = value; +}; + +const assertNotBlobDoesNotExistCheck = (value: Blob | File | string) => { + // @ts-expect-error -- `File` extends `Blob` in TypeScript but does not match the exact runtime `Blob` check. + isAssert.not.blob(value); // eslint-disable-line @typescript-eslint/no-unsafe-call + const _: Blob | File | string = value; +}; + +const assertNotMapDoesNotExistCheck = (value: Map | string) => { + // @ts-expect-error -- Structural object types such as `Map` can be assignable in TypeScript without matching the runtime brand check. + isAssert.not.map(value); // eslint-disable-line @typescript-eslint/no-unsafe-call, unicorn/no-array-callback-reference + const _: Map | string = value; +}; + +const assertNotSetDoesNotExistCheck = (value: Set | string) => { + // @ts-expect-error -- Structural object types such as `Set` can be assignable in TypeScript without matching the runtime brand check. + isAssert.not.set(value); // eslint-disable-line @typescript-eslint/no-unsafe-call + const _: Set | string = value; +}; + +const assertNotDateDoesNotExistCheck = (value: Date | string) => { + // @ts-expect-error -- Structural object types such as `Date` can be assignable in TypeScript without matching the runtime brand check. + isAssert.not.date(value); // eslint-disable-line @typescript-eslint/no-unsafe-call + const _: Date | string = value; +}; + +// Suppress unused variable warnings +nanCheck(42); +finiteNumberCheck(42); +nonNegativeNumberCheck(42); +positiveIntegerCheck(42); +negativeIntegerCheck(-1); +nonNegativeIntegerCheck(0); +infiniteCheck(Number.POSITIVE_INFINITY); +integerCheck(1); +safeIntegerCheck(1); +evenIntegerCheck(2); +oddIntegerCheck(1); +positiveNumberCheck(1); +negativeNumberCheck(-1); +validLengthCheck(0); +integerUnknownCheck(1); +positiveIntegerUnknownCheck(1); +integerMixedUnionCheck(1); +positiveNumberMixedUnionCheck(1); +chainedNumericGuardCheck(1); +distinctNumericBrandsStayDistinct(42 as PositiveInteger, -1 as NegativeInteger, 0 as ValidLength); +assertNotUndefinedCheck('🦄'); +assertNotUndefinedUnknownCheck('🦄'); +assertNotUndefinedGenericCheck('🦄'); +assertNotNullUnknownCheck('🦄'); +assertNotNullOrUndefinedCheck('🦄'); +assertNotNullOrUndefinedUnknownCheck('🦄'); +assertNotStringCheck(1); +assertNotStringUnknownCheck(1); +assertNotStringGenericCheck(1); +assertNotBooleanUnknownCheck(1); +assertNotSymbolUnknownCheck(1); +assertNotBigintUnknownCheck(1); +assertNotPrimitiveUnknownCheck({}); +assertNotPrimitiveGenericCheck({unicorn: true}); +assertNotNamedUndefinedExportCheck(0); +assertNotNamedNullOrUndefinedUnknownExportCheck('🦄'); +assertNotNamedStringExportCheck(1); +assertNotNamedStringUnknownExportCheck(1); +assertNotNamedPrimitiveUnknownExportCheck({}); +assertNotCallableDoesNotExistCheck('🦄'); +assertNotNumberDoesNotExistCheck(Number.NaN); +assertNotIntegerDoesNotExistCheck(1.5); +assertNotObjectDoesNotExistCheck('🦄'); +assertNotBlobDoesNotExistCheck('🦄'); +assertNotMapDoesNotExistCheck('🦄'); +assertNotSetDoesNotExistCheck('🦄'); +assertNotDateDoesNotExistCheck('🦄'); diff --git a/tsconfig.json b/tsconfig.json index 0aace6f..46013a6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,5 +1,11 @@ { "extends": "@sindresorhus/tsconfig", + "compilerOptions": { + "types": ["node"], + "rootDir": "source", + "allowImportingTsExtensions": true, + "rewriteRelativeImportExtensions": true + }, "include": [ "source" ],