Skip to content

@studnicky/predicates

Type guards, numeric checks, string bounds, array constraints, and coercion helpers aligned to JSON Schema draft 2020-12.

Install

bash
pnpm add @studnicky/predicates

Requires @studnicky:registry=https://npm.pkg.github.com in .npmrc.

Usage

Type checks

ts
import { Predicates } from '../src/index.js';

// inferValueType maps JS values to JSON Schema type names
const nullType = Predicates.inferValueType(null);
const arrayType = Predicates.inferValueType([]);
const objectType = Predicates.inferValueType({});
const stringType = Predicates.inferValueType('hello');
const numberType = Predicates.inferValueType(42);
const booleanType = Predicates.inferValueType(true);

console.log('inferValueType(null):', nullType);     // 'null'
console.log('inferValueType([]):', arrayType);      // 'array'
console.log('inferValueType({}):', objectType);     // 'object'
console.log('inferValueType(hello):', stringType);  // 'string'
console.log('inferValueType(42):', numberType);     // 'number'
console.log('inferValueType(true):', booleanType);  // 'boolean'

// isFiniteNumber and isIntegerValue narrow numeric primitives
console.log('isFiniteNumber(42):', Predicates.isFiniteNumber(42));         // true
console.log('isFiniteNumber(Infinity):', Predicates.isFiniteNumber(Infinity)); // false
console.log('isIntegerValue(3):', Predicates.isIntegerValue(3));           // true
console.log('isIntegerValue(3.14):', Predicates.isIntegerValue(3.14));     // false

// matchesType dispatches to per-type matchers
console.log('matchesType integer 5:', Predicates.matchesType('integer', 5));    // true
console.log('matchesType number 5.5:', Predicates.matchesType('number', 5.5));  // true
console.log('matchesType object []:', Predicates.matchesType('object', []));    // false

// matchesAnyType is a union check: true if any type in the list matches
console.log('matchesAnyType string|null null:', Predicates.matchesAnyType(['string', 'null'], null)); // true
console.log('matchesAnyType string|null 42:', Predicates.matchesAnyType(['string', 'null'], 42));    // false

String validation

ts
// codePointLength counts Unicode scalar values, not UTF-16 code units
console.log('codePointLength hello:', Predicates.codePointLength('hello'));   // 5
console.log('codePointLength 👋:', Predicates.codePointLength('👋'));          // 1
console.log('codePointLength 👋👋:', Predicates.codePointLength('👋👋'));      // 2

// satisfiesMinLength / satisfiesMaxLength operate on code-point counts
console.log('satisfiesMinLength hello 5:', Predicates.satisfiesMinLength('hello', 5));  // true
console.log('satisfiesMinLength hi 5:', Predicates.satisfiesMinLength('hi', 5));        // false
console.log('satisfiesMaxLength hello 5:', Predicates.satisfiesMaxLength('hello', 5));  // true
console.log('satisfiesMaxLength hello 4:', Predicates.satisfiesMaxLength('hello', 4));  // false

// satisfiesPattern accepts a pre-compiled RegExp
console.log('satisfiesPattern abc123:', Predicates.satisfiesPattern('abc123', WORD_PATTERN));  // true
console.log('satisfiesPattern abc 123:', Predicates.satisfiesPattern('abc 123', WORD_PATTERN)); // false

Numeric validation

ts
// checkMinimum / checkMaximum with inclusive and exclusive modes
console.log('checkMinimum 5 5 false:', Predicates.checkMinimum(5, 5, false));   // true  (inclusive)
console.log('checkMinimum 5 5 true:', Predicates.checkMinimum(5, 5, true));     // false (exclusive)
console.log('checkMaximum 10 10 false:', Predicates.checkMaximum(10, 10, false)); // true
console.log('checkMaximum 10 10 true:', Predicates.checkMaximum(10, 10, true));   // false

// checkMultipleOf is epsilon-tolerant for floating-point rounding
console.log('checkMultipleOf 0.3 0.1:', Predicates.checkMultipleOf(0.3, 0.1)); // true
console.log('checkMultipleOf 9 3:', Predicates.checkMultipleOf(9, 3));         // true
console.log('checkMultipleOf 10 3:', Predicates.checkMultipleOf(10, 3));       // false

Array validation

ts
// satisfiesMinItems / satisfiesMaxItems test array length
console.log('satisfiesMinItems [1,2,3] 3:', Predicates.satisfiesMinItems([1, 2, 3], 3));  // true
console.log('satisfiesMinItems [1,2] 3:', Predicates.satisfiesMinItems([1, 2], 3));        // false
console.log('satisfiesMaxItems [1,2] 3:', Predicates.satisfiesMaxItems([1, 2], 3));        // true
console.log('satisfiesMaxItems [1,2,3,4] 3:', Predicates.satisfiesMaxItems([1, 2, 3, 4], 3)); // false

// satisfiesUniqueItems uses deep equality for each pair
console.log('satisfiesUniqueItems [1,2,3]:', Predicates.satisfiesUniqueItems([1, 2, 3]));        // true
console.log('satisfiesUniqueItems [1,2,1]:', Predicates.satisfiesUniqueItems([1, 2, 1]));        // false
console.log('satisfiesUniqueItems [{a:1},{a:1}]:', Predicates.satisfiesUniqueItems([{ 'a': 1 }, { 'a': 1 }])); // false

// satisfiesContains validates minContains/maxContains bounds against a pre-counted match total
console.log('satisfiesContains 2 1 3:', Predicates.satisfiesContains(2, 1, 3));              // true
console.log('satisfiesContains 0 1 3:', Predicates.satisfiesContains(0, 1, 3));              // false
console.log('satisfiesContains 1 undef undef:', Predicates.satisfiesContains(1, undefined, undefined)); // true

Object validation

ts
// hasAllRequiredProperties verifies all listed keys exist in the object
console.log('hasAllRequiredProperties {a,b} [a,b]:', Predicates.hasAllRequiredProperties({ 'a': 1, 'b': 2 }, ['a', 'b'])); // true
console.log('hasAllRequiredProperties {a} [a,b]:', Predicates.hasAllRequiredProperties({ 'a': 1 }, ['a', 'b']));            // false

// hasNoAdditionalProperties checks that every key belongs to the allowed set
console.log('hasNoAdditionalProperties {a} Set[a,b]:', Predicates.hasNoAdditionalProperties({ 'a': 1 }, new Set(['a', 'b']))); // true
console.log('hasNoAdditionalProperties {a,c} Set[a,b]:', Predicates.hasNoAdditionalProperties({ 'a': 1, 'c': 3 }, new Set(['a', 'b']))); // false

// satisfiesMinProperties / satisfiesMaxProperties count own enumerable keys
console.log('satisfiesMinProperties {a,b} 2:', Predicates.satisfiesMinProperties({ 'a': 1, 'b': 2 }, 2)); // true
console.log('satisfiesMaxProperties {a,b,c} 2:', Predicates.satisfiesMaxProperties({ 'a': 1, 'b': 2, 'c': 3 }, 2)); // false

Enum and const

ts
// satisfiesEnum uses deep equality against each candidate
console.log('satisfiesEnum red:', Predicates.satisfiesEnum('red', ['red', 'green', 'blue']));         // true
console.log('satisfiesEnum yellow:', Predicates.satisfiesEnum('yellow', ['red', 'green', 'blue']));   // false
console.log('satisfiesEnum {x:1}:', Predicates.satisfiesEnum({ 'x': 1 }, [{ 'x': 1 }, { 'x': 2 }])); // true

// satisfiesConst uses deep equality for a single fixed value
console.log('satisfiesConst {x:1} {x:1}:', Predicates.satisfiesConst({ 'x': 1 }, { 'x': 1 })); // true
console.log('satisfiesConst {x:1} {x:2}:', Predicates.satisfiesConst({ 'x': 1 }, { 'x': 2 })); // false

Coercion

ts
// coerceToBoolean: recognised literals only; undefined for anything else
console.log('coerceToBoolean true:', Predicates.coerceToBoolean('true'));    // true
console.log('coerceToBoolean 1:', Predicates.coerceToBoolean('1'));          // true
console.log('coerceToBoolean maybe:', Predicates.coerceToBoolean('maybe'));  // undefined

// coerceToNumber: finite numbers only; undefined for NaN / Infinity / non-numeric
console.log('coerceToNumber 3.14:', Predicates.coerceToNumber('3.14'));      // 3.14
console.log('coerceToNumber NaN:', Predicates.coerceToNumber('NaN'));        // undefined
console.log('coerceToNumber Infinity:', Predicates.coerceToNumber('Infinity')); // undefined

// coerceValue tries each schema type in order, returns first successful coercion
const coercedInt = Predicates.coerceValue(['integer', 'string'], '42');
console.log('coerceValue integer|string 42:', coercedInt);  // 42

Content encoding and media type

ts
// satisfiesContentEncoding validates base64 and base64url; unknown encodings pass
console.log('satisfiesContentEncoding base64:', Predicates.satisfiesContentEncoding('aGVsbG8=', 'base64'));            // true
console.log('satisfiesContentEncoding bad base64:', Predicates.satisfiesContentEncoding('not-base64!!!', 'base64'));   // false
console.log('satisfiesContentEncoding base64url:', Predicates.satisfiesContentEncoding('aGVsbG8', 'base64url'));       // true
console.log('satisfiesContentEncoding unknown:', Predicates.satisfiesContentEncoding('anything', 'custom-encoding'));  // true

// satisfiesContentMediaType validates application/json; unknown media types pass
console.log('satisfiesContentMediaType json:', Predicates.satisfiesContentMediaType('{"x":1}', 'application/json'));    // true
console.log('satisfiesContentMediaType bad json:', Predicates.satisfiesContentMediaType('{bad json}', 'application/json')); // false
console.log('satisfiesContentMediaType text/plain:', Predicates.satisfiesContentMediaType('anything', 'text/plain'));   // true

Try it

JSON Schema type inference and numeric guards
/** typeInference — inferValueType, isFiniteNumber, isIntegerValue, matchesType, matchesAnyType. Run: npx tsx examples/typeInference.ts */

import assert from 'node:assert/strict';

// #region usage
import { Predicates } from '../src/index.js';

// inferValueType maps JS values to JSON Schema type names
const nullType = Predicates.inferValueType(null);
const arrayType = Predicates.inferValueType([]);
const objectType = Predicates.inferValueType({});
const stringType = Predicates.inferValueType('hello');
const numberType = Predicates.inferValueType(42);
const booleanType = Predicates.inferValueType(true);

console.log('inferValueType(null):', nullType);     // 'null'
console.log('inferValueType([]):', arrayType);      // 'array'
console.log('inferValueType({}):', objectType);     // 'object'
console.log('inferValueType(hello):', stringType);  // 'string'
console.log('inferValueType(42):', numberType);     // 'number'
console.log('inferValueType(true):', booleanType);  // 'boolean'

// isFiniteNumber and isIntegerValue narrow numeric primitives
console.log('isFiniteNumber(42):', Predicates.isFiniteNumber(42));         // true
console.log('isFiniteNumber(Infinity):', Predicates.isFiniteNumber(Infinity)); // false
console.log('isIntegerValue(3):', Predicates.isIntegerValue(3));           // true
console.log('isIntegerValue(3.14):', Predicates.isIntegerValue(3.14));     // false

// matchesType dispatches to per-type matchers
console.log('matchesType integer 5:', Predicates.matchesType('integer', 5));    // true
console.log('matchesType number 5.5:', Predicates.matchesType('number', 5.5));  // true
console.log('matchesType object []:', Predicates.matchesType('object', []));    // false

// matchesAnyType is a union check: true if any type in the list matches
console.log('matchesAnyType string|null null:', Predicates.matchesAnyType(['string', 'null'], null)); // true
console.log('matchesAnyType string|null 42:', Predicates.matchesAnyType(['string', 'null'], 42));    // false
// #endregion usage

assert.equal(nullType, 'null');
assert.equal(arrayType, 'array');
assert.equal(objectType, 'object');
assert.equal(stringType, 'string');
assert.equal(numberType, 'number');
assert.equal(booleanType, 'boolean');

assert.equal(Predicates.isFiniteNumber(42), true);
assert.equal(Predicates.isFiniteNumber(Infinity), false);
assert.equal(Predicates.isFiniteNumber(NaN), false);
assert.equal(Predicates.isFiniteNumber('42'), false);
assert.equal(Predicates.isIntegerValue(3), true);
assert.equal(Predicates.isIntegerValue(3.14), false);

assert.equal(Predicates.matchesType('null', null), true);
assert.equal(Predicates.matchesType('array', [1, 2]), true);
assert.equal(Predicates.matchesType('integer', 5), true);
assert.equal(Predicates.matchesType('integer', 5.5), false);
assert.equal(Predicates.matchesType('number', 5.5), true);
assert.equal(Predicates.matchesType('object', {}), true);
assert.equal(Predicates.matchesType('object', []), false);
assert.equal(Predicates.matchesType('string', 'x'), true);
assert.equal(Predicates.matchesType('boolean', false), true);

assert.equal(Predicates.matchesAnyType(['string', 'null'], null), true);
assert.equal(Predicates.matchesAnyType(['string', 'null'], 'hi'), true);
assert.equal(Predicates.matchesAnyType(['string', 'null'], 42), false);
assert.equal(Predicates.matchesAnyType(['integer', 'boolean'], true), true);

console.log('typeInference: all assertions passed');
Output
Press Execute to run this example against the real library.

The output shows inferValueType mapping JS values to JSON Schema type names, isFiniteNumber/isIntegerValue narrowing numerics, and matchesType/matchesAnyType performing union-aware dispatch.

API

ExportTypeDescription
PredicatesclassStatic-only predicate and coercion library
CoerceToBooleanResultTypetypeboolean | undefined
CoerceToNumberResultTypetypenumber | undefined

Selected Predicates static methods

MethodDescription
isFiniteNumber(value)True for finite number values
isIntegerValue(value)True for integer number values
inferValueType(value)Returns JSON Schema type name ('null', 'array', 'object', etc.)
matchesType(schemaType, value)True if value satisfies the named JSON Schema type
matchesAnyType(schemaTypes, value)True if value satisfies any type in the list
coerceToBoolean(value)Coerces string to boolean; returns undefined for unrecognised literals
coerceToNumber(value)Coerces string to finite number; returns undefined for non-numeric input
coerceValue(schemaTypes, value)Tries each coercer in order; returns first successful result
codePointLength(str)Counts Unicode code points without allocating
satisfiesMinLength(str, min)Fast-path code-point length check
satisfiesMaxLength(str, max)Fast-path code-point length check
checkMultipleOf(value, divisor)Epsilon-tolerant floating-point multiple check
satisfiesUniqueItems(arr)Deep-equal uniqueness check
satisfiesContains(matchCount, min, max)Validates minContains/maxContains bounds
satisfiesContentEncoding(value, encoding)Validates base64/base64url encoding
satisfiesContentMediaType(value, mediaType, encoding?)Validates application/json content

Source on GitHub