/** @namespace types */
// @flow
/**
* One common way to determine the type of class that you are working with,
* in a fairly compatible manner, is to use .call or .apply on the function
* toString of the Object.prototype.
*
* Calling `Object.prototype.toString.call('hello')` will yield
* `"[object String]"` as an answer. This technique is fairly sound but is
* also fairly verbose to use often. This function extracts the detected value
* name from the above string; so "String" from "[object String]" and so forth.
*
* The added advantage of using this method is that it works well with direct
* name comparisons, such as `typeOf("asdfas") === String.name`. The new
* `Symbol.toStringTag` allows you to define custom values that are
* reflected in this manner.
*
* @method ⌾⠀typeOf
* @memberof types
* @inner
*
* @param {any} object any value is acceptable here, including null and
* undefined
* @return {string} for objects of type [object String] the value "String"
* will be returned.
*/
export function typeOf(object: any): string {
return /(\b\w+\b)\]/.exec(Object.prototype.toString.call(object))[1];
}
/**
* Returns true if the type supplied evaluates to `[object Function]`
*
* @method ⌾⠀isFunction
* @memberof types
* @inner
*
* @param {any} obj any object that can be passed to Object.prototype.toString
* @return {Boolean} true if it passes the test, false otherwise
*/
export const isFunction = (obj) => typeOf(obj) === Function.name
/**
* Returns true if the type supplied evaluates to `[object Array]`
*
* @method ⌾⠀isArray
* @memberof types
* @inner
*
* @param {any} obj any object that can be passed to Object.prototype.toString
* @return {Boolean} true if it passes the test, false otherwise
*/
export const isArray = (obj) => typeOf(obj) === Array.name
/**
* Returns true if the type supplied evaluates to `[object Date]`
*
* @method ⌾⠀isDate
* @memberof types
* @inner
*
* @param {any} obj any object that can be passed to Object.prototype.toString
* @return {Boolean} true if it passes the test, false otherwise
*/
export const isDate = (obj) => typeOf(obj) === Date.name
/**
* Returns true if the type supplied evaluates to `[object Object]`
*
* @method ⌾⠀isObject
* @memberof types
* @inner
*
* @param {any} obj any object that can be passed to Object.prototype.toString
* @return {Boolean} true if it passes the test, false otherwise
*/
export const isObject = (obj) => typeOf(obj) === Object.name
/**
* Returns true if the type supplied evaluates to `[object String]`
*
* @method ⌾⠀isString
* @memberof types
* @inner
*
* @param {any} obj any object that can be passed to Object.prototype.toString
* @return {Boolean} true if it passes the test, false otherwise
*/
export const isString = (obj) => typeOf(obj) === String.name
/**
* Returns true if the type supplied evaluates to `[object Number]`
*
* @method ⌾⠀isNumber
* @memberof types
* @inner
*
* @param {any} obj any object that can be passed to Object.prototype.toString
* @return {Boolean} true if it passes the test, false otherwise
*/
export const isNumber = (obj) => typeOf(obj) === isNumber.name
/**
* Returns true if the type supplied evaluates to `[object RegExp]`
*
* @method ⌾⠀isRegExp
* @memberof types
* @inner
*
* @param {any} obj any object that can be passed to Object.prototype.toString
* @return {Boolean} true if it passes the test, false otherwise
*/
export const isRegExp = (obj) => typeOf(obj) === RegExp.name
/**
* Returns true if the type supplied evaluates to `[object Null]`
*
* @method ⌾⠀isNull
* @memberof types
* @inner
*
* @param {any} obj any object that can be passed to Object.prototype.toString
* @return {Boolean} true if it passes the test, false otherwise
*/
export const isNull = (obj) => typeOf(obj) === NULL
/**
* Returns true if the type supplied evaluates to `[object Undefined]`
*
* @method ⌾⠀isUndefined
* @memberof types
* @inner
*
* @param {any} obj any object that can be passed to Object.prototype.toString
* @return {Boolean} true if it passes the test, false otherwise
*/
export const isUndefined = (obj) => typeOf(obj) === UNDEFINED
/**
* Determines if the resulting type is one of the six types of primitives
* (according to MDN; https://goo.gl/USmkUU). If it is, true will be returned;
* otherwise false.
*
* @method ⌾⠀isPrimitive
* @memberof types
* @inner
*
* @return {Boolean} true if not one of Boolean, Null, Undefined, Number,
* String or Symbol.
*/
export const isPrimitive = (obj) => PRIMITIVES.has(obj)
/**
* Returns true if the type supplied evaluates to neither `[object Object]`
* nor `[object Array]`.
*
* @method ⌾⠀isValue
* @memberof types
* @inner
*
* @param {any} obj any object that can be passed to Object.prototype.toString
* @return {Boolean} true if it passes the test, false otherwise
*/
export const isValue = (obj) => !isObject(obj) && !isArray(obj)
/**
* A shorthand way to test an object's declared toString type to a supplied
* string or Function/Class. Realistically, this checks typeOf(obj) to both
* T and T.name. If either are true, then true is returned; false otherwise.
*
* @method ⌾⠀isOfType
* @memberof types
* @inner
*
* @param {any} obj any object that can be passed to Object.prototype.toString
* @return {Boolean} true if it passes the test, false otherwise
*/
export const isOfType = (obj, T) => typeOf(obj) === T || typeOf(obj) === T.name
/**
* Returns true if the supplied obj is a ECMAScript class definition. It first
* checks by examining the properties of the supplied class. Secondly it checks
* by searching the toString() method of the 'function' for the term class. If
* either are true, then true is returned; false is returned otherwise.
*
* NOTE Relying on this strictly, especially when used with other libraries
* can cause some problems down the line, especially if the code wraps a class
* instance like react-jss or other similar use cases. Use at your own peril.
*
* @method ⌾⠀isClass
* @memberof types
* @inner
*
* @param {mixed} obj any object who's type is to be compared as a class
* @return {boolean} true if the obj is an ECMAScript class object; not an
* instance. False otherwise.
*
* @see #isNativeClassByProps
* @see #isNativeClassByString
*/
export const isClass = (obj) => (
isNativeClassByProps(obj) ||
isNativeClassByString(obj))
/**
* isNativeClass method taken from code submitted on stackoverflow. Logic and
* basis for the test appears there. See URL below for follow up if desired.
*
* @see https://stackoverflow.com/questions/29093396/how-do-you-check-the-difference-between-an-ecmascript-6-class-and-function#32235645
*
* @method ⌾⠀isNativeClassByProps
* @memberof types
* @inner
*
* @param {mixed} thing any type of JavaScript value to test
* @return {boolean} true if it is a ECMAScript class by testing properties;
* false otherwise
*/
export function isNativeClassByProps(thing: mixed): boolean {
return (
typeof thing === 'function'
&& thing.hasOwnProperty('prototype')
&& !thing.hasOwnProperty('arguments'))
}
/**
* isNativeClass method taken from code submitted on stackoverflow. Logic and
* basis for the test appears there. See URL below for follow up if desired.
*
* @see https://stackoverflow.com/questions/29093396/how-do-you-check-the-difference-between-an-ecmascript-6-class-and-function#32235645
*
* @method ⌾⠀isNativeClassByString
* @memberof types
* @inner
*
* @param {mixed} thing any type of JavaScript value to test
* @return {Boolean} true if it is a ECMAScript class by testing properties;
* false otherwise
*/
export function isNativeClassByString(value: mixed):boolean {
return (
typeof value === 'function'
&& value.toString().indexOf('class') === 0)
}
/**
* NOTE This function will not work on nodejs versions less than 6 as Reflect
* is needed natively.
*
* The instanceof keyword only works on instances of an object and not on
* the class objects the instances are created from.
*
* ```js
* class A {}
* class B extends A {}
*
* let a = new A();
* let b = new B();
*
* b instanceof A; // true
* a instanceof A; // true
* B instanceof A; // false
* ```
*
* Therefore the extendsFrom function checks this relationship at the class
* level and not at the instance level.
*
* ```js
* import { extendsFrom } from '...'
*
* class A {}
* class B extends A {}
* class C extends B {}
*
* extendsFrom(A, A); // true
* extendsFrom(B, A); // true
* extendsFrom(C, A); // true
* extendsFrom(C, 1); // false
* extendsFrom(B, null); // false
* ```
*
* @method ⌾⠀extendsFrom
* @memberof types
* @inner
*
* @param {Function} TestedClass the class of which to test heredity
* @param {Function} RootClass the ancestor to test for
* @param {Boolean} enforceClasses if true, false by default, an additional
* runtime check for the type of the supplied Class objects will be made. If
* either is not a Function, an error is thrown.
* @return {Boolean} true if the lineage exists; false otherwise
*
* @see types#isClass
*/
export function extendsFrom(
TestedClass: Function,
RootClass: Function,
enforceClasses: boolean = false
): boolean {
if (parseInt(process.version.substring(1)) < 6) {
throw new Error(`
Reflect must be implemented in the JavaScript engine. This cannot be
polyfilled and as such, if process.version is less than 6 an error will
be thrown. Please try an alternate means of doing what you desire.
`);
}
if (enforceClasses) {
if (!isClass(TestedClass) && !isClass(RootClass)) {
throw new Error(`
When using extendsFrom() with enforceClasses true, each Function
argument supplied must pass the isClass() method testing. See the
function isClass to learn more about these requirements.
`);
}
}
if (!TestedClass || !RootClass) { return false; }
let proto = TestedClass;
while (true) {
try {
if (proto === RootClass) return true;
if (proto === Function) break;
proto = Reflect.getPrototypeOf(proto);
}
catch (ignore) {
return false;
}
}
return false;
}
/**
* Programmatic constant defintion of the result of a call to
* `typeOf(undefined)`.
*
* @memberof types
* @type {string}
* @const
*/
export const UNDEFINED: string = typeOf(undefined);
/**
* Programmatic constant defintion of the result of a call to
* `typeOf(null)`.
*
* @memberof types
* @type {string}
* @const
*/
export const NULL: string = typeOf(null);
/**
* Create a base set containing the typeOf representations for each of the
* known primitive types.
*
* @type {Set<String>}
* @memberof types
* @inner
*/
const PRIMITIVES: Set<string> = new Set([
NULL, UNDEFINED, Boolean.name, Number.name, String.name, Symbol.name
]);
/** Store the original has() method and bind it to PRIMITIVES */
PRIMITIVES[Symbol.for('original_has')] = PRIMITIVES.has.bind(PRIMITIVES)
/**
* Modify the PRIMITIVES `has()` method to invoke `typeOf()` on the argument
* before passing it to the underlying has() method originally passed down from
* the Set.prototype.
*
* @method has
* @memberof PRIMITIVES
* @inner
*
* @param {mixed} o any value to test to see if it qualifies as a primitive
* @return {Boolean} true if the supplied value is a primitive, false otherwise
*/
PRIMITIVES.has = (o) => PRIMITIVES[Symbol.for('original_has')](typeOf(o))
/**
* When testing if a type is a primitive, it is often easier to simply verify
* that with a list of known types. To make this dead simple, a modified `Set`
* containing the `typeOf` results for each of the six known JavaScript
* primitive types is exported.
*
* The modifications are such that a call to `has()`, on this Set only, first
* converts the supplied values to their resulting `typeOf()` representations.
* So, `PRIMITIVES.has(4)` would be the same as `PRIMITIVES.has('Number')`.
*
* @memberof types
* @type {Set<string>}
* @const
*/
export { PRIMITIVES };