Created
February 22, 2024 19:11
-
-
Save Eunomiac/6c566dd3606546d8fb8ed5ccfb5d6e00 to your computer and use it in GitHub Desktop.
realTypeOf() --- Enhanced JavaScript type checker extending typeof with precise definitions for numbers, objects, functions, and more.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* @module realTypeOf | |
* @version 1.0.0 | |
* @date 2024-02-22 | |
* @author Ryan West ("Eunomiac") | |
* @discord Eunomiac#8172 | |
* @github https://github.com/Eunomiac | |
* @codepen https://codepen.io/eunomiac | |
* | |
* @description This module provides enhanced type determination functionalities for JavaScript values. | |
* It exports a single function, `{@link realTypeOf}`, which extends the basic `typeof` operator capabilities | |
* by offering more specific type definitions, such as distinguishing between integers and floats, async | |
* and arrow functions, generator functions, and various object types including arrays, null objects, and | |
* plain object literals. The module's behavior can be customized through the `{@link REALTYPEOF_CONFIG}` | |
* configuration object, allowing users to specify which distinctions should be made. | |
* | |
* For comprehensive tests covering various scenarios and configurations, see `realTypeOf.tests.js`. | |
* | |
* The core functionality is centered around the `{@link realTypeOf}` function, which uses a series of helper | |
* functions to accurately determine the type of a given value. These helper functions include: | |
* - `{@link getNumberType}` for distinguishing between different types of numeric values. | |
* - `{@link getObjectType}` for identifying specific object types. | |
* - `{@link getFunctionType}` for differentiating between function types, including async and arrow functions. | |
* - `{@link getSymbolType}` for classifying symbols based on their descriptions. | |
* | |
* This module is designed to be flexible and extensible, with the ability to easily adjust which | |
* types are distinguished through the `{@link REALTYPEOF_CONFIG}` configuration object. It aims to provide | |
* developers with more precise tools for type checking and validation in JavaScript applications. | |
* | |
* With default configuration settings, possible return values (sorted by typeof check) are: | |
* | |
* typeof ref === "number": "int", "float", "NaN", "infinite", | |
* typeof ref === "object": "null", "array", "plain_object", the name of the object's constructor | |
* typeof ref === "function": "constructor_function", "function" | |
* typeof ref === "symbol": "symbol" | |
* typeof ref === "bigint": "bigint" | |
* typeof ref === "undefined": "undefined" | |
* typeof ref === "boolean": "boolean" | |
* typeof ref === "string": "string" | |
* | |
* With ALL configuration checks enabled (and isStrict is disabled, allowing "unknown" return types), possible | |
* return values are: | |
* | |
* typeof ref === "number": "int", "float", "NaN", "infinite", | |
* typeof ref === "object": "null", "array", "plain_object", the name of the object's constructor | |
* "null_object", "unknown_object" | |
* typeof ref === "function": "constructor_function", "function", | |
* "arrow_function", "async_function", "async_arrow_function", "generator_function" | |
* typeof ref === "symbol": "<description>_symbol", "anonymous_symbol" | |
* (where 'description' is the description provided when the symbol was created) | |
* typeof ref === "bigint": "bigint" | |
* typeof ref === "undefined": "undefined" | |
* typeof ref === "boolean": "boolean" | |
* typeof ref === "string": "string" | |
* | |
* @example | |
* import realTypeOf from "./realTypeOf.mjs"; | |
* class SampleClass { } | |
* | |
* SIMPLE TYPES: | |
* console.log(realTypeOf("foo")); // "string" | |
* console.log(realTypeOf({foo: "bar"})); // "plain_object" | |
* console.log(realTypeOf(Object.create(null))); // "null_object" (or "plain_object", depending on settings) | |
* console.log(realTypeOf([1, 2, 3])); // "array" | |
* console.log(realTypeOf(null)) // "null" | |
* console.log(realTypeOf(undefined)); // "undefined" | |
* console.log(realTypeOf(true)); // "boolean" | |
* | |
* NUMBERS: | |
* console.log(realTypeOf(1)); // "int" | |
* console.log(realTypeOf(1.5)); // "float" | |
* console.log(realTypeOf(1 - undefined)); // "NaN" | |
* console.log(realTypeOf(1 / 0)); // "infinite" | |
* console.log(realTypeOf(12345890n)); // "bigint" (or "int", depending on settings) | |
* | |
* CLASSES & CLASS INSTANCES: | |
* | |
* console.log(realTypeOf(SampleClass)); // "constructor_function" | |
* console.log(realTypeOf(new SampleClass())); // "SampleClass" | |
* | |
* FUNCTIONS: | |
* console.log(realTypeOf(function foo() { return "bar"; })); // "function" | |
* console.log(realTypeOf(() => "bar")); // "arrow_function" | |
* console.log(realTypeOf(async function() { return "foo"; })); // "async_function" | |
* console.log(realTypeOf(async () => { return "foo"; })); // "async_arrow_function" | |
* console.log(realTypeOf(function*() { yield 1; })); // "generator_function" | |
* Note: Configuration settings allow collapsing some or all of these types into "function". | |
* | |
* SYMBOLS: | |
* console.log(realTypeOf(Symbol("foo"))); // "foo_symbol" | |
* console.log(realTypeOf(Symbol.iterator)); // "iterator_symbol" | |
* console.log(realTypeOf(Symbol())); // "anonymous_symbol" | |
* Note: Configuration settings allow collapsing some or all of these types into "symbol". | |
* | |
* @see {@link realTypeOf} for the primary type-checking function. | |
* @see {@link REALTYPEOF_CONFIG} for important settings defining the specificity of type determination. | |
*/ | |
/** | |
* @typedef {Object} RealTypeOfConfig | |
* @description This object contains settings for defining the specificity of type | |
* definitions returned by the {@link realTypeOf} function. These settings can be enabled or disabled | |
* for customization, allowing developers to tailor the type determination to their specific needs. | |
* To disable a specific setting, comment out the respective property or set its value to 'false'. | |
* | |
* The configuration is designed to provide flexibility in how types are distinguished, offering | |
* options to differentiate between similar types (e.g., "bigint" vs. "int") or to recognize specific | |
* function types (e.g., "arrow_function" vs. "function"). This enables more precise type checking | |
* and validation in JavaScript applications. | |
* | |
* === Settings ENABLED by Default === | |
* @property {boolean} IS_DISTINGUISHING_BIGINT - Distinguish "bigint" from "int". | |
* If disabled, both are returned as "int". This is useful | |
* for applications that need to differentiate between | |
* these two numeric types for validation or processing logic. | |
* @property {boolean} IS_DISTINGUISHING_CONSTRUCTOR_FUNCTION - Distinguish "constructor_function" from | |
* "function". If disabled, both are returned as "function". | |
* This distinction is important for identifying functions | |
* that are intended to be used with the 'new' keyword. | |
* @property {boolean} IS_DISTINGUISHING_ARROW_FUNCTION - Distinguish "arrow_function" from "function" (and | |
* "async_arrow_function" from "async_function", if | |
* distinguishing async functions). If disabled, all are returned as "function". | |
* This setting is particularly useful for code analysis tools | |
* or when specific function types need to be handled differently. | |
* @remarks | |
* The `IS_DISTINGUISHING_ARROW_FUNCTION` option relies on heuristics based on function properties | |
* and syntax patterns. In typical development and runtime environments, if you are not using | |
* advanced or unconventional JavaScript features, this setting should work as expected. Do not | |
* rely on this functionality for code that has been minified or transpiled where such transformations | |
* would alter the expected syntax for defining arrow functions. | |
* | |
* === Settings DISABLED by Default === | |
* @property {boolean} IS_DISTINGUISHING_NULL_OBJECT - Distinguish objects with a null prototype ("null_object") | |
* from plain objects. This can be useful for detecting objects created | |
* with `Object.create(null)`, which do not inherit from `Object.prototype`. | |
* If disabled, returns "plain_object" for both. | |
* @property {boolean} IS_DISTINGUISHING_ASYNC_FUNCTION - Distinguish "async_function" from "function" (and | |
* "async_arrow_function" from "arrow_function", if enabled). | |
* If disabled, returns "function" (or "arrow_function"). | |
* This distinction helps in understanding the asynchronous nature | |
* of functions, which can be critical for debugging or runtime analysis. | |
* @property {boolean} IS_DISTINGUISHING_GENERATOR_FUNCTION - Distinguish "generator_function" from "function". | |
* If disabled, returns "function" for both. | |
* Identifying generator functions can be important for runtime | |
* behavior analysis, especially in contexts where the function's | |
* control flow is relevant. | |
* @property {boolean} IS_DISTINGUISHING_SYMBOL_TYPES - Distinguish "<description>_symbol" and "anonymous_symbol" | |
* from "symbol". If disabled, returns "symbol". | |
* This allows for more descriptive debugging information by providing | |
* the symbol's description in its type, when available. | |
*/ | |
export const REALTYPEOF_CONFIG = { | |
IS_DISTINGUISHING_BIGINT: true, | |
IS_DISTINGUISHING_CONSTRUCTOR_FUNCTION: true, | |
IS_DISTINGUISHING_ARROW_FUNCTION: true, | |
// IS_DISTINGUISHING_NULL_OBJECT: true, | |
// IS_DISTINGUISHING_ASYNC_FUNCTION: true, | |
// IS_DISTINGUISHING_GENERATOR_FUNCTION: true, | |
// IS_DISTINGUISHING_SYMBOL_TYPES: true | |
}; | |
/** | |
* Determines the type of a number with more specificity than the basic typeof check. | |
* @param {number} ref - The number to determine the type of. | |
* @returns {RealType} - The specific type of the number: "NaN", "infinite", "int", or "float". | |
*/ | |
function getNumberType(ref) { | |
if (Number.isNaN(ref)) | |
return "NaN"; // Check for Not-a-Number | |
if (!Number.isFinite(ref)) | |
return "infinite"; // Check for infinity | |
if (Number.isInteger(ref)) | |
return "int"; // Check for integer | |
return "float"; // Default to float if none of the above | |
} | |
/** | |
* Determines the type of an object with more specificity, considering various characteristics. | |
* @param {object|null} ref - The object to determine the type of. | |
* @param {boolean} isStrict - Determines whether to throw an error for unrecognized types. | |
* If false, returns "unknown_object" for unrecognized "object" types. | |
* @returns {RealType} - The specific type of the object, e.g., "null", "array", "plain_object", constructor name. | |
*/ | |
function getObjectType(ref, isStrict) { | |
if (ref === null) | |
return "null"; // Check for null | |
if (Array.isArray(ref)) | |
return "array"; // Check for array | |
// Check for objects with a null prototype | |
if (Object.getPrototypeOf(ref) === null) { | |
return REALTYPEOF_CONFIG.IS_DISTINGUISHING_NULL_OBJECT ? "null_object" : "plain_object"; | |
} | |
// Default to plain_object if prototype is Object.prototype | |
if (Object.getPrototypeOf(ref) === Object.prototype) | |
return "plain_object"; | |
// Use constructor name if available | |
if (typeof ref?.constructor?.name === "string") | |
return ref.constructor.name; | |
// Throw error if type is unrecognized and strict mode is enabled | |
if (isStrict) { | |
throw new Error(`Unrecognized "object" Type: ${String(ref)}`); | |
} | |
return "unknown_object"; // Fallback for unrecognized objects | |
} | |
/** | |
* Determines the type of a function with more specificity, considering async, arrow, and generator distinctions. | |
* @param {Function} ref - The function to determine the type of. | |
* @returns {RealType} - The specific type of the function, e.g., "function", "async_function", "arrow_function". | |
*/ | |
function getFunctionType(ref) { | |
if (typeof ref !== "function") { | |
return undefined; | |
} | |
const typeParts = ["function"]; // Initialize with base type | |
// Check for generator function | |
if (REALTYPEOF_CONFIG.IS_DISTINGUISHING_GENERATOR_FUNCTION | |
&& ref.constructor.name === "GeneratorFunction") { | |
typeParts.unshift("generator"); | |
// Check for constructor function | |
} | |
else if (REALTYPEOF_CONFIG.IS_DISTINGUISHING_CONSTRUCTOR_FUNCTION | |
&& (Object.keys(ref.prototype || {}).length > 0 | |
|| /^class\s/.test(ref.toString()))) { | |
typeParts.unshift("constructor"); | |
} | |
else { | |
// Check for arrow function | |
if (REALTYPEOF_CONFIG.IS_DISTINGUISHING_ARROW_FUNCTION | |
&& ref.prototype === undefined) { | |
const funcStr = ref.toString().trim(); // Convert function to string for pattern matching | |
// Pattern match for arrow function syntax | |
if (/^(?:async\s*)?\((?:\s*[_$\w]+\s*,?)*\)\s*=>/.test(funcStr) | |
|| /^[_$\w]+\s*=>/.test(funcStr) | |
|| /^async\s+[_$\w]+\s*=>/.test(funcStr)) { | |
typeParts.unshift("arrow"); // Prepend "arrow" to type | |
} | |
} | |
// Check for async function | |
if (REALTYPEOF_CONFIG.IS_DISTINGUISHING_ASYNC_FUNCTION | |
&& ref.constructor.name === "AsyncFunction") { | |
typeParts.unshift("async"); // Prepend "async" to type | |
} | |
} | |
return typeParts.join("_"); // Join type parts with an underscore | |
} | |
/** | |
* Determines the type of a symbol, potentially distinguishing based on the symbol's description. | |
* @param {symbol} ref - The symbol to determine the type of. | |
* @returns {RealType} - The type of the symbol, depending on configuration settings | |
* e.g., "symbol", "<description>_symbol", or "anonymous_symbol". | |
* NOTE: The "Symbol." prefix for built-in JavaScript symbols is stripped | |
* from the return type. E.g. getSymbolType(Symbol.iterator) returns "iterator_symbol". | |
*/ | |
function getSymbolType(ref) { | |
// Return "symbol" for all symbols if not distinguishing symbol descriptions | |
if (!REALTYPEOF_CONFIG.IS_DISTINGUISHING_SYMBOL_TYPES) { | |
return "symbol"; | |
} | |
// Extracts the description or "anonymous" if none, and remove redundant "Symbol." prefix. | |
const symbolDescription = (String(ref).slice(7, -1) || "anonymous") | |
.replace(/^Symbol\./, ""); | |
return `${symbolDescription}_symbol`; | |
} | |
/** | |
* Determines the specific type of a given reference in a detailed manner beyond the basic typeof check. | |
* This function can distinguish between various object types, function types, and primitives with greater specificity. | |
* | |
* @param {unknown} ref - The reference for which the type is to be determined. | |
* @param {boolean} [isStrict=true] - Determines whether to throw an error for unrecognized types. | |
* If false, returns "unknown_object" (for unrecognized "object" types) | |
* or "unknown" (for any other unrecognized types). | |
* @returns {string} - A string representing the specific type of the reference. | |
* | |
* @throws {Error} - Throws an error if `isStrict` is true and the type cannot be recognized. | |
* | |
* @see {@link REALTYPEOF_CONFIG} for the configuration object that defines the specificity of type determination. | |
*/ | |
export default function realTypeOf(ref, isStrict = true) { | |
switch (typeof ref) { | |
case "number": return getNumberType(ref); | |
case "object": return getObjectType(ref, isStrict); | |
case "function": return getFunctionType(ref); | |
case "symbol": return getSymbolType(ref); | |
case "bigint": return REALTYPEOF_CONFIG.IS_DISTINGUISHING_BIGINT ? "bigint" : "int"; | |
case "string": return "string"; | |
case "boolean": return "boolean"; | |
case "undefined": return "undefined"; | |
default: | |
// Handle unrecognized types based on the isStrict flag | |
if (isStrict) | |
throw new Error(`Unrecognized Type: ${typeof ref}`); | |
return "unknown"; | |
} | |
} | |
/** | |
* @license MIT License | |
* | |
* Permission is hereby granted, free of charge, to any person obtaining a copy | |
* of this software and associated documentation files (the "Software"), to deal | |
* in the Software without restriction, including without limitation the rights | |
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
* copies of the Software, and to permit persons to whom the Software is | |
* furnished to do so, subject to the following conditions: | |
* | |
* The above copyright notice and this permission notice shall be included in all | |
* copies or substantial portions of the Software. | |
* | |
* THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
* IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
* FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
* AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
* LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
* OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
* SOFTWARE. | |
*/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
import realTypeOf, { REALTYPEOF_CONFIG } from "./realTypeOf"; | |
/** | |
* realTypeOf.test.js | |
* | |
* This file contains the Jest test suite for the `realTypeOf` module, testing its functionality | |
* across a variety of inputs to ensure accurate type determination. These tests cover both default | |
* and non-default configuration settings to ensure comprehensive coverage. | |
* | |
* To run these tests, ensure you have Jest installed and configured in your project. | |
* Usage: `jest realTypeOf.test.js` | |
* | |
* @requires jest - The testing framework used to run the tests. | |
* @requires realTypeOf - The module being tested. | |
* @requires REALTYPEOF_CONFIG - The configuration object used by `realTypeOf` for custom type determination. | |
*/ | |
/** | |
* This is a helper function, which reverses the values of all config settings | |
* for testing purposes. | |
* | |
* @returns {void} | |
*/ | |
function reverseConfig() { | |
const CONFIG_KEYS = [ | |
"IS_DISTINGUISHING_BIGINT", | |
"IS_DISTINGUISHING_CONSTRUCTOR_FUNCTION", | |
"IS_DISTINGUISHING_ARROW_FUNCTION", | |
"IS_DISTINGUISHING_NULL_OBJECT", | |
"IS_DISTINGUISHING_ASYNC_FUNCTION", | |
"IS_DISTINGUISHING_GENERATOR_FUNCTION", | |
"IS_DISTINGUISHING_SYMBOL_TYPES" | |
]; | |
for (const key of CONFIG_KEYS) { | |
if (key in REALTYPEOF_CONFIG && REALTYPEOF_CONFIG[key]) { | |
delete REALTYPEOF_CONFIG[key]; | |
} | |
else { | |
REALTYPEOF_CONFIG[key] = true; | |
} | |
} | |
} | |
class MyClass { } | |
describe("realTypeOf", () => { | |
// Backup of the original configuration | |
let originalConfig = {}; | |
beforeEach(() => { | |
// Deep clone the original configuration to ensure nested objects are copied | |
originalConfig = JSON.parse(JSON.stringify(REALTYPEOF_CONFIG)); | |
}); | |
afterEach(() => { | |
// Clear current configuration | |
Object.keys(REALTYPEOF_CONFIG).forEach(key => delete REALTYPEOF_CONFIG[key]); | |
// Restore the original configuration | |
Object.assign(REALTYPEOF_CONFIG, originalConfig); | |
}); | |
// Test for simple types (default configuration) | |
it("should correctly identify simple types (default configuration settings)", () => { | |
expect(realTypeOf("foo")).toBe("string"); | |
expect(realTypeOf(true)).toBe("boolean"); | |
expect(realTypeOf(undefined)).toBe("undefined"); | |
expect(realTypeOf(null)).toBe("null"); | |
expect(realTypeOf(1)).toBe("int"); | |
expect(realTypeOf(1.5)).toBe("float"); | |
expect(realTypeOf(1235n)).toBe("bigint"); | |
expect(realTypeOf(1 / 0)).toBe("infinite"); | |
expect(realTypeOf(0 / 0)).toBe("NaN"); | |
expect(realTypeOf(Symbol("test"))).toBe("symbol"); | |
expect(realTypeOf(Symbol())).toBe("symbol"); | |
expect(realTypeOf(Symbol.iterator)).toBe("symbol"); | |
}); | |
// Test for simple types using non-default configuration settings | |
it("should correctly identify simple types (NON-default configuration settings)", () => { | |
reverseConfig(); | |
expect(realTypeOf(1235n)).toBe("int"); | |
expect(realTypeOf(Symbol("test"))).toBe("test_symbol"); | |
expect(realTypeOf(Symbol())).toBe("anonymous_symbol"); | |
expect(realTypeOf(Symbol.iterator)).toBe("iterator_symbol"); | |
}); | |
// Test for object types (default configuration) | |
it("should correctly identify object types (default configuration settings)", () => { | |
expect(realTypeOf({})).toBe("plain_object"); | |
expect(realTypeOf([])).toBe("array"); | |
expect(realTypeOf(Object.create(null))).toBe("plain_object"); | |
expect(realTypeOf(MyClass)).toBe("constructor_function"); | |
expect(realTypeOf(new MyClass())).toBe("MyClass"); | |
}); | |
// Test for object types (NON-default configuration) | |
it("should correctly identify object types (NON-default configuration settings)", () => { | |
reverseConfig(); | |
expect(realTypeOf(Object.create(null))).toBe("null_object"); | |
expect(realTypeOf(MyClass)).toBe("function"); | |
}); | |
// Test for function types (default configuration) | |
it("should correctly identify function types (default configuration settings)", () => { | |
expect(realTypeOf(function () { })).toBe("function"); | |
expect(realTypeOf(() => { })).toBe("arrow_function"); | |
expect(realTypeOf(async function () { })).toBe("function"); | |
expect(realTypeOf(async () => { })).toBe("arrow_function"); | |
expect(realTypeOf(function* () { })).toBe("function"); | |
}); | |
it("should correctly identify function types (NON-default configuration settings)", () => { | |
reverseConfig(); | |
expect(realTypeOf(() => { })).toBe("function"); | |
expect(realTypeOf(async function () { })).toBe("async_function"); | |
expect(realTypeOf(async () => { })).toBe("async_function"); | |
expect(realTypeOf(function* () { })).toBe("generator_function"); | |
}); | |
it("should correctly handle combinations of async and arrow functions when both settings are enabled", () => { | |
// Adjust the configuration for this test case | |
REALTYPEOF_CONFIG.IS_DISTINGUISHING_ARROW_FUNCTION = true; | |
REALTYPEOF_CONFIG.IS_DISTINGUISHING_ASYNC_FUNCTION = true; | |
expect(realTypeOf(() => true)).toBe("arrow_function"); | |
expect(realTypeOf(async function () { })).toBe("async_function"); | |
expect(realTypeOf(async () => { })).toBe("async_arrow_function"); | |
}); | |
}); |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment