Skip to content

Instantly share code, notes, and snippets.

@emmaly
Last active December 26, 2022 05:14
Show Gist options
  • Save emmaly/58e542a8b680655e9d767c103b0002e5 to your computer and use it in GitHub Desktop.
Save emmaly/58e542a8b680655e9d767c103b0002e5 to your computer and use it in GitHub Desktop.
JavaScript type match helper
[
{
"desc": "primative string",
"asString": "something",
"matches": [
"has:toString",
"has:valueOf",
"has:slice",
"has:constructor",
"string"
]
},
{
"desc": "String object, non-primative",
"asString": "something",
"matches": [
"has:toString",
"has:valueOf",
"has:slice",
"has:constructor",
"object",
"String"
]
},
{
"desc": "primative number",
"asString": "42",
"matches": [
"has:toString",
"has:valueOf",
"has:constructor",
"has:toFixed",
"number",
"number:valid"
]
},
{
"desc": "plain object",
"asString": "[object Object]",
"matches": [
"has:toString",
"has:valueOf",
"has:constructor",
"object",
"Object"
]
},
{
"desc": "plain object",
"asString": "[object Object]",
"matches": [
"has:toString",
"has:valueOf",
"has:constructor",
"object",
"Object"
]
},
{
"desc": "Number()",
"asString": "0",
"matches": [
"has:toString",
"has:valueOf",
"has:constructor",
"has:toFixed",
"number",
"number:valid"
]
},
{
"desc": "new Number()",
"asString": "0",
"matches": [
"has:toString",
"has:valueOf",
"has:constructor",
"has:toFixed",
"object",
"Number",
"Number:valid"
]
},
{
"desc": "Infinity (which is a special number)",
"asString": "Infinity",
"matches": [
"has:toString",
"has:valueOf",
"has:constructor",
"has:toFixed",
"number",
"number:valid",
"Infinity"
]
},
{
"desc": "NaN (which a special number, which is not a number)",
"asString": "NaN",
"matches": [
"has:toString",
"has:valueOf",
"has:constructor",
"has:toFixed",
"number",
"NaN"
]
},
{
"desc": "Date()",
"asString": "Sun Dec 25 2022 21:13:30 GMT-0800 (Pacific Standard Time)",
"matches": [
"has:toString",
"has:valueOf",
"has:slice",
"has:constructor",
"string"
]
},
{
"desc": "new Date()",
"asString": "Sun Dec 25 2022 21:13:30 GMT-0800 (Pacific Standard Time)",
"matches": [
"has:toString",
"has:valueOf",
"has:constructor",
"object",
"Date",
"Date:valid"
]
},
{
"desc": "String().toString (which is a native function)",
"matches": [
"has:toString",
"has:valueOf",
"has:constructor",
"function",
"function:native"
]
},
{
"desc": "isType (which is the function this is running through)",
"matches": [
"has:toString",
"has:valueOf",
"has:constructor",
"function"
]
}
]
const testingAgainstTheseTypes = [
"has:toString",
"has:valueOf",
"has:tacos",
"has:slice",
"has:constructor",
"has:toFixed",
"function",
"function:native",
"object",
"Object",
"string",
"String",
"Number",
"Number:valid",
"number",
"number:valid",
"Infinity",
"NaN",
"Date",
"Date:valid",
];
const results = [
["something", "primative string"],
[new String("something"), "String object, non-primative"],
[42, "primative number"],
[{}, "plain object"],
[new Object(), "plain object"],
[Number(), "Number()"],
[new Number(), "new Number()"],
[Infinity, "Infinity (which is a special number)"],
[NaN, "NaN (which a special number, which is not a number)"],
[Date(), "Date()"],
[new Date(), "new Date()"],
[String().toString, "String().toString (which is a native function)"],
[isType, "isType (which is the function this is running through)"],
]
.map(([o, desc]) => {
const out = {};
out.desc = desc;
if (isType(o, "has:toString") && !isType(o, "function"))
out.asString = o.toString();
out.matches = isType(o, testingAgainstTheseTypes, false, true);
return out;
});
JSON.stringify(results, null, 2);
/**
* Returns `true` if `o` matches any of the `types`.
* `matchAll` modifies this behavior.
* `returnMatched` modifies it even more.
*
* @param {any} o
* The variable to test against.
*
* @param {TestableType|TestableType[]} types
* A type or list of types to test against.
* They can be a list of classes to match against as well.
* The list provided in `TestableTypes` is only a suggestion.
*
* @param {boolean} matchAll
* Changes the behavior to require that all `types` match against `o`
* in order to return `true`, rather than the default behavior of
* simply matching at least one of the provided `types`.
*
* Ignored if `returnMatched` is `true`.
*
* Default is `false`.
*
* @param {boolean} returnMatched
* Ignore what I said. If you set this `true`, it'll just give you
* an array of all matches.
*
* If you set this to `true` then `matchAll` is ignored altogether.
*
* Default is `false`.
*
* @returns {boolean|string[]}
* * If `returnMatched` is `false` (default), returns `boolean`.
* * If `returnMatched` is `true`, returns `string[]`.
*/
function isType(o, types, matchAll, returnMatched) {
matchAll = matchAll === true; // sets to `false` if missing or wrong type
returnMatched = returnMatched === true; // sets to `false` if missing or wrong type
const matches = (
Array.isArray(types)
? types // supply the already-ready array
: [types] // supply a new single-item array
).filter((type) => {
// `type` is supposed to be a string, but since it's not,
// perhaps we're being asked for an `instanceof` test...
if (typeof type === "object") return o instanceof type;
// get our base type
const baseType = typeof o;
// the most simple of tests:
// does the requested `type` match its base type?
if (type === baseType) return true;
// `number:valid`: a `number` & a validity test.
if (type === "number:valid") return (baseType === "number" && !isNaN(o));
// `Number:valid`: a `Number` (not a `number`) & a validity test.
if (type === "Number:valid") return (baseType === "object" && o instanceof Number && !isNaN(o));
// `Infinity`: a `number`, which is a special number that's somewhat large.
if (type === "Infinity") return (baseType === "number" && o === Infinity);
// `NaN`: a `number`, which a special number, which is not a number.
if (type === "NaN") return (baseType === "number" && isNaN(o));
// `has:*` property test
if (typeof type === "string" && type.startsWith('has:'))
return o !== null && o !== undefined && o[type.replace(/^has:/, '')] !== undefined;
// only object and function baseTypes test beyond this point...
// ... so if it isn't one of those, let's bail now.
if (!["object","function"].includes(baseType)) return false;
// ~~ no primitive types (except null) beyond this point ~~
// `null`: it's a special duck!
// It's a primitive... but also `typeof null === 'object'`.
if (type === "null") return (baseType === "object" && o === null);
// `Date`: an instance of `Date` is good enough.
if (type === "Date") return (baseType === "object" && o instanceof Date);
// `Date:valid`: an instance of `Date` & a validity test.
if (type === "Date:valid") return (baseType === "object" && o instanceof Date && !isNaN(o));
// `Array`: is an `Array`
if (type === "Array") return (baseType === "object" && Array.isArray(o));
// `class`: a `function` with source code starting with the word `class`.
if (type === "class")
return (
baseType === "function" &&
Function.prototype.toString.call(o).startsWith("class")
);
// `function:native`: a `function` with source code containing `native code` and no line breaks.
if (type === "function:native")
return (
baseType === "function" &&
/^\s*function\s*[^\n\{]+\{[^\n\{]+native code[^\n\}]+\}[^\n\}]*$/
.test(Function.prototype.toString.call(o))
);
// `type` === `className` perhaps...
const className = o.constructor.name;
if (type === className) return true;
// fail: no match
return false;
});
// if returnMatched=true, just return matches
if (returnMatched === true) return matches;
// nothing matched always returns false
if (!matches || !Array.isArray(matches) || matches.length === 0)
return false;
return matchAll === true
? matches.length === types.length // ALL matches are required to give `true`
: matches.length > 0; // ANY matches are good enough for `true`
}
// these are just suggestions and examples...
const TestableTypes = {
"undefined" : "undefined",
"null" : "null",
"object" : "object",
"Object" : "Object",
"boolean" : "boolean",
"Boolean" : "Boolean",
"number" : "number",
"number:valid" : "number:valid",
"Number" : "Number",
"Number:valid" : "Number:valid",
"NaN" : "NaN",
"Infinity" : "Infinity",
"bigint" : "bigint",
"BigInt64Array" : "BigInt64Array",
"BigUint64Array" : "BigUint64Array",
"string" : "string",
"String" : "String",
"symbol" : "symbol",
"function" : "function",
"function:native" : "function:native",
"Date" : "Date",
"Date:valid" : "Date:valid",
"Array" : "Array",
"Boolean" : "Boolean",
"class" : "class",
"has:toString" : "has:toString",
"has:toValue" : "has:toValue",
"has:constructor" : "has:constructor",
};
/**
* @typedef {keyof TestableTypes} TestableType
*/
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment