From 54fc09406a5e8edf44726fdea02ed4658d6b3db2 Mon Sep 17 00:00:00 2001 From: Sindre Sorhus Date: Wed, 8 Apr 2026 18:58:23 +0700 Subject: [PATCH] Add `negativeInteger`, `nonNegativeInteger`, `arrayOf`, and `oneOf` predicates --- readme.md | 30 ++++++++++++++ source/index.ts | 43 ++++++++++++++++++++ test/test.ts | 102 +++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 173 insertions(+), 2 deletions(-) diff --git a/readme.md b/readme.md index 5f82bbf..b2fb284 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) @@ -483,6 +494,14 @@ Check if `value` is a number and is 0 or more. 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. @@ -661,6 +680,17 @@ 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. diff --git a/source/index.ts b/source/index.ts index 5f563a5..1a9308d 100644 --- a/source/index.ts +++ b/source/index.ts @@ -133,7 +133,12 @@ const assertionTypeDescriptions = [ 'non-empty map', 'PropertyKey', 'even integer', + 'finite number', + 'negative integer', + 'non-negative integer', + 'non-negative number', 'odd integer', + 'positive integer', 'T', 'in range', 'predicate returns truthy for any value', @@ -240,6 +245,7 @@ const is = Object.assign( array: isArray, arrayBuffer: isArrayBuffer, arrayLike: isArrayLike, + arrayOf: isArrayOf, asyncFunction: isAsyncFunction, asyncGenerator: isAsyncGenerator, asyncGeneratorFunction: isAsyncGeneratorFunction, @@ -284,6 +290,7 @@ const is = Object.assign( map: isMap, nan: isNan, nativePromise: isNativePromise, + negativeInteger: isNegativeInteger, negativeNumber: isNegativeNumber, nodeStream: isNodeStream, nonEmptyArray: isNonEmptyArray, @@ -292,6 +299,7 @@ const is = Object.assign( nonEmptySet: isNonEmptySet, nonEmptyString: isNonEmptyString, nonEmptyStringAndNotWhitespace: isNonEmptyStringAndNotWhitespace, + nonNegativeInteger: isNonNegativeInteger, nonNegativeNumber: isNonNegativeNumber, null: isNull, nullOrUndefined: isNullOrUndefined, @@ -300,6 +308,7 @@ const is = Object.assign( object: isObject, observable: isObservable, oddInteger: isOddInteger, + oneOf: isOneOf, plainObject: isPlainObject, positiveInteger: isPositiveInteger, positiveNumber: isPositiveNumber, @@ -435,6 +444,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'; } @@ -648,6 +661,10 @@ export function isNativePromise(value: unknown): value is Promise= 0; +} + export function isNonNegativeNumber(value: unknown): value is number { return isNumber(value) && value >= 0; } @@ -733,6 +754,10 @@ export function isOddInteger(value: unknown): value is number { 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 as T[number]); +} + 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) { @@ -925,7 +950,9 @@ type Assert = { 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; @@ -1086,6 +1113,7 @@ export const assert: Assert = { map: assertMap, nan: assertNan, nativePromise: assertNativePromise, + negativeInteger: assertNegativeInteger, negativeNumber: assertNegativeNumber, nodeStream: assertNodeStream, nonEmptyArray: assertNonEmptyArray, @@ -1094,6 +1122,7 @@ export const assert: Assert = { nonEmptySet: assertNonEmptySet, nonEmptyString: assertNonEmptyString, nonEmptyStringAndNotWhitespace: assertNonEmptyStringAndNotWhitespace, + nonNegativeInteger: assertNonNegativeInteger, nonNegativeNumber: assertNonNegativeNumber, null: assertNull, nullOrUndefined: assertNullOrUndefined, @@ -1180,6 +1209,7 @@ const methodTypeMap = { isMap: 'Map', isNan: 'NaN', isNativePromise: 'native Promise', + isNegativeInteger: 'negative integer', isNegativeNumber: 'negative number', isNodeStream: 'Node.js Stream', isNonEmptyArray: 'non-empty array', @@ -1188,6 +1218,7 @@ 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', @@ -1553,6 +1584,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)); @@ -1601,6 +1638,12 @@ export function assertNonEmptyStringAndNotWhitespace(value: unknown, message?: s } } +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)); diff --git a/test/test.ts b/test/test.ts index fce8b4d..11ab0df 100644 --- a/test/test.ts +++ b/test/test.ts @@ -666,6 +666,69 @@ test('is.positiveInteger', () => { }); }); +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.numericString supplemental', () => { assert.strictEqual(is.numericString(''), false); assert.strictEqual(is.numericString(' '), false); @@ -713,6 +776,41 @@ test('is.array supplemental', () => { }, /Expected numbers/v); }); +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.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); + + 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 @@ -1359,11 +1457,11 @@ test('is.inRange', () => { }); assert.throws(() => { - is.inRange(5, [NaN, 10]); + is.inRange(5, [Number.NaN, 10]); }, TypeError); assert.throws(() => { - is.inRange(5, [0, NaN]); + is.inRange(5, [0, Number.NaN]); }, TypeError); assert.doesNotThrow(() => {