Skip to content

Instantly share code, notes, and snippets.

@Eunomiac
Created February 22, 2024 19:11
Show Gist options
  • Save Eunomiac/6c566dd3606546d8fb8ed5ccfb5d6e00 to your computer and use it in GitHub Desktop.
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.
/**
* @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.
*/
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