UNPKG

@moyal/js-type

Version:

Minimal, robust type detection and value classification utility for JavaScript.

463 lines (413 loc) 16.2 kB
/** * @module * @ignore */ /** * @ignore */ class BuildInfo { static version = "1.0.3"; } /*! * @moyal/js-type - Minimal, robust type detection and value classification utility for JavaScript. * * File: moyal.linq.js * Repository: https://github.com/IlanMoyal/moyal.js.type * Author: Ilan Moyal (https://www.moyal.es) * Contact: ilan.amoyal[guess...what]gmail.com * * Description: * moyal.js.type is a lightweight, dependency-free utility library that provides accurate and extensible type-checking functions for JavaScript values, across both browser and Node.js environments. It helps you safely determine value types, including edge cases like user-defined classes, boxed primitives, cross-realm objects, and complex built-in types such as Map, Set, Date, and Promise. * Unlike typeof, instanceof, or loose type tricks, this library uses stable techniques like Object.prototype.toString.call(...) and constructor introspection to reliably classify values, even across different JavaScript contexts (e.g., iframes or VMs). * * License: * MIT License – Permission is granted for free use, modification, and distribution, * provided that the copyright notice and license appear in all copies. * Full license text: https://opensource.org/licenses/MIT * * © 2000–present Ilan Moyal. All rights reserved. */ const __primitives = ["string", "number", "bigint", "boolean", "symbol"]; const __object_toString = Object.prototype.toString; /** * Returns the semantic version of this library. * @returns {string} - The semantic version of this library. */ function version() { return BuildInfo.version; } /** * Determines whether the given function is a user defined class constructor. * Inspects the function’s string to determine if it's a class constructor. * * @param {*} fn - The function to test. * @returns {boolean} `true` if the given function is a user defined class constructor. */ function isClass(fn) { /* assume value is a function */ const str = Function.prototype.toString.call(fn); return /^class\s/.test(str); } /** * Returns the type name of a given value. * - For primitives: returns `"string"`, `"number"`, etc. * - For objects: returns class name or `"object"` if plain. * - For built-in types like Date, RegExp, Map: returns the constructor name. * - For plain objects: returns "object". For arrays: "array". * - For classes: returns `"class"` for user defined classes. * - For functions: returns `"function"` * * @param {*} value - The value whose type name to determine. * @returns {string} The detected type name. */ function getTypeName(value) { if(value === null) return "null"; let name = typeof value; if (name === "function") { if(isClass(value)) name = "class"; } else if (name === "object") { name = __object_toString.call(value).slice(8, -1).toLowerCase(); if(!__primitives.includes(name)) { const ctor = value.constructor; if (ctor && typeof ctor.name === "string") { name = ctor.name; if(name === "Object" || name === "Array") name = name.toLowerCase(); } } } return name; } /** * Checks if the specified value matches the given type name. * @param {*} value - The value to test. * @param {string} typeName - The expected type name. * @returns {boolean} `true` if the value’s type matches the given name. */ function isType(value, typeName) { return getTypeName(value) === typeName; } /** * Checks if the specified value is a string (primitive or String object). * @param {*} value - The value to test. * @returns {boolean} `true` if the specified value is a string. */ function isString(value) { return getTypeName(value) === "string"; } /** * Checks if the specified value is a symbol. * @param {*} value - The value to test. * @returns {boolean} `true` if the specified value is a symbol. */ function isSymbol(value) { return getTypeName(value) === "symbol"; } /** * Checks whether the specified value is a number (primitive or Number object). * Excludes NaN and Infinity by default unless explicitly allowed. * * @param {*} value - The value to test. * @param {Object} [options] * @param {boolean} [options.allowNaN=true] - Whether to consider NaN a valid number. * @param {boolean} [options.allowInfinity=true] - Whether to consider Infinity/-Infinity valid. * @returns {boolean} - `true` if the specified value is a number. */ function isNumber(value, { allowNaN = true, allowInfinity = true } = {}) { if (getTypeName(value) !== "number") return false; const num = Number(value); if (!allowNaN && Number.isNaN(num)) return false; if (!allowInfinity && !Number.isFinite(num)) return false; return true; } /** * Checks if the specified value is a bigint (primitive or BigInt object). * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is big integer. */ function isBigInt(value) { return getTypeName(value) === "bigint"; } /** * Checks if the value is a numeric type (either number or bigint). * @param {*} value * @returns {boolean} - `true` if the value is a numeric type (either number or bigint). */ function isNumeric(value) { return isNumber(value) || isBigInt(value); } /** * Checks if the specified value is a boolean (primitive or Boolean object). * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is boolean. */ function isBoolean(value) { return getTypeName(value) === "boolean"; } /** * Checks if the specified value is undefined. * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is undefined. */ function isUndefined(value) { return value === undefined; } /** * Checks if the specified value is null. * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is null. */ function isNull(value) { return value === null; } /** * Checks if the specified value is a standard function. * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is Function. */ function isFunction(value) { return getTypeName(value) === "function"; } /** * Checks if the specified value is a user defined class. * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is user defined class. */ function isUserDefinedClass(value) { return getTypeName(value) === "class"; } /** * Checks if the specified value is a plain object (i.e., created via `{}` or `new Object()`). * This works reliably across realms. * * @param {*} value - The value to check. * @returns {boolean} `true` if it's a plain object. */ function isPlainObject(value) { if (__object_toString.call(value) !== "[object Object]") return false; const proto = Object.getPrototypeOf(value); return proto === Object.prototype || proto === null; } /** * Checks if the specified value is a non-wrapper object (i.e., not a function, not null, not a boxed primitive). * Includes objects like arrays, plain objects, Date, RegExp, etc. * * @param {*} value - The value to check. * @returns {boolean} `true` if it's a real object, excluding primitive wrappers and functions. */ function isObject(value) { if (value === null || typeof value !== "object" || Array.isArray(value)) return false; // Exclude known boxed primitive wrappers const tag = __object_toString.call(value); return !["[object String]", "[object Number]", "[object Boolean]", "[object BigInt]", "[object Symbol]", "[object Function]"].includes(tag); } /** * Checks if the specified value is instance of Date. * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is instance of Date. */ function isDate(value) { return getTypeName(value) === "Date"; /* cross realm safe. instanceof is NOT cross realm safe! */} /** * Checks if the specified value is an array. * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is an array. */ function isArray(value) { return Array.isArray(value); } /** * Checks if the specified value is of Error type. * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is of Error type. */ function isError(value) { return value != null && __object_toString.call(value) === "[object Error]"; // Works on cross realm /* Note: * obj instanceof Error; * works only in same realm (same frame!) */ } /** * Determines whether the specified value is one of the primitive types: string, number, bigint, boolean and symbol (including wrapper type over primitive) * This function treats primitives any of the following: string, number, boolean, symbol, bigint and their wrapper String, Number, Boolean. * @param {*} value * @returns {boolean} - true if the specified value is one of the primitive types: string, number, bigint, boolean and symbol (including wrapper type over primitive) */ function isPrimitive(value) { return value != null && __primitives.includes(getTypeName(value)); } /** * Checks if the value is an integer (number or bigint), optionally applying a custom predicate. * @param {*} value - The value to test. * @param {function(number):boolean} [additionalPredicate = null] - Additional test to apply to the value. * @returns {boolean} - `true` if the value is integral number and passed the test against the additional predicate (if it was specified) . */ function isIntegral(value, additionalPredicate = null) { const isIntegral = isBigInt(value) || (isNumber(value) && Math.floor(value) === value); return isIntegral && (additionalPredicate == null || additionalPredicate(value) === true); } /** * Checks if the specified value is iterable (has a `[Symbol.iterator]` method). * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is iterable (has a `[Symbol.iterator]` method). */ function isIterable(value) { return isFunctionOrGeneratorFunction(value?.[Symbol.iterator]); } /** * Checks if the specified value is a function or a generator function. * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is a function or a generator function. */ function isFunctionOrGeneratorFunction(value) { const type = typeof value; const tag = __object_toString.call(value); return (type === "object" || type === "function") && (tag === "[object Function]" || tag === "[object GeneratorFunction]"); } /** * Checks if the specified value is a generator function. * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is a generator function. */ function isGeneratorFunction(value) { return __object_toString.call(value) === "[object GeneratorFunction]"; } /** * Checks if the specified value is an asynchronous function. * @param {*} value - The value to test. * @returns {boolean} - `true` if the specified value is an asynchronous function. */ function isAsyncFunction(value) { return __object_toString.call(value) === "[object AsyncFunction]"; } /** * Parses common values like string "true" → boolean true, "42.0" → number, etc. * @param {*} value * @returns {InferDataTypeResult} */ function inferDataType(value) { const type = getTypeName(value); if (type !== "string") { return new InferDataTypeResult(value, value, type); } const trimmed = value.trim(); if (trimmed === "true" || trimmed === "false") { return new InferDataTypeResult(value, trimmed === "true", "boolean"); } const parsed = parseFloat(trimmed); if (!isNaN(parsed)) { // normalize both to Number→String comparison to avoid fragile regex cleanup const parsedStr = parsed.toString(); const inputNumeric = Number(trimmed).toString(); if (parsedStr === inputNumeric) { return new InferDataTypeResult(value, parsed, "number"); } } return new InferDataTypeResult(value, value, "string"); } /** * Parses a string to boolean. If the input is already boolean, returns it. * If it's "true" or "false" (case insensitive), converts accordingly. * @param {*} value - The value to parse. * @returns {boolean|undefined} - 'true' if the specified value is a string trimmed to literal 'true' (case insensitive) or `false` if it is trimmed to literal 'false' (case insensitive). */ function parseBool(value) { if (isBoolean(value)) return value; if (isString(value)) { const lower = value.trim().toLowerCase(); if (lower === "true") return true; if (lower === "false") return false; } return undefined; } /** * Represents the result of inferred data type conversion. */ class InferDataTypeResult { /** * @param {*} originalValue * @param {*} parsedValue * @param {string|null} type */ constructor(originalValue, parsedValue, type) { /** @type {*} */ this.originalValue = originalValue; /** @type {*} */ this.parsedValue = parsedValue; /** @type {string|null} */ this.type = type; } } /** * Checks if the specified value is a RegExp. * @param {*} value - The value to test. * @returns {boolean} `true` if the value is a RegExp. */ function isRegExp(value) { return getTypeName(value) === "RegExp"; } /** * Checks if the specified value is a Map. * @param {*} value - The value to test. * @returns {boolean} `true` if the value is a Map. */ function isMap(value) { return getTypeName(value) === "Map"; } /** * Checks if the specified value is a Set. * @param {*} value - The value to test. * @returns {boolean} `true` if the value is a Set. */ function isSet(value) { return getTypeName(value) === "Set"; } /** * Checks if the specified value is a WeakMap. * @param {*} value - The value to test. * @returns {boolean} `true` if the value is a WeakMap. */ function isWeakMap(value) { return getTypeName(value) === "WeakMap"; } /** * Checks if the specified value is a WeakSet. * @param {*} value - The value to test. * @returns {boolean} `true` if the value is a WeakSet. */ function isWeakSet(value) { return getTypeName(value) === "WeakSet"; } /** * Checks if the specified value is a Promise. * @param {*} value - The value to test. * @returns {boolean} `true` if the value is a Promise. */ function isPromise(value) { return getTypeName(value) === "Promise"; } /** * Checks if the specified value is "empty". * The following are considered empty: * - null, undefined * - empty string: "" * - empty array: [] * - empty object: {} * - empty Map or Set * - Note: The implementation avoids treating numbers or booleans as "empty". Only typical "container-like" values and nullish types are evaluated. * * @param {*} value - The value to test. * @returns {boolean} `true` if the value is considered empty. */ function isEmpty(value) { if (value == null) return true; // null or undefined if (isString(value) || Array.isArray(value)) { return value.length === 0; } if (isMap(value) || isSet(value)) { return value.size === 0; } if (isPlainObject(value)) { return Object.keys(value).length === 0; } return false; } /** * Checks if the specified value is not empty. * See `isEmpty` for full empty definition. * @param {*} value - The value to test. * @returns {boolean} `true` if the value is not considered empty. */ function isNotEmpty(value) { return !isEmpty(value); } export { InferDataTypeResult, getTypeName, inferDataType, isArray, isAsyncFunction, isBigInt, isBoolean, isDate, isEmpty, isError, isFunction, isFunctionOrGeneratorFunction, isGeneratorFunction, isIntegral, isIterable, isMap, isNotEmpty, isNull, isNumber, isNumeric, isObject, isPlainObject, isPrimitive, isPromise, isRegExp, isSet, isString, isSymbol, isType, isUndefined, isUserDefinedClass, isWeakMap, isWeakSet, parseBool, version };