Skip to content

Instantly share code, notes, and snippets.

@webia1
Forked from rauschma/README.md
Created July 10, 2022 14:41
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save webia1/b1227a3dab6b04f3bd2ac2d44df17ceb to your computer and use it in GitHub Desktop.
Save webia1/b1227a3dab6b04f3bd2ac2d44df17ceb to your computer and use it in GitHub Desktop.

hasType(): function and protocol for dynamic type checks

The problem

Depending on what kind of type check we want to perform, we have to choose between:

  • Does a value v have a given primitive type – e.g.:
    typeof v === 'string'
  • Is a value v an object (which includes objects that are not instances of Object such as the result of Object.create(null))?
    v !== null && (typeof v === 'object' || typeof v === 'function')
  • Is a value v in the current realm an instance of a class C in the current realm?
    • v instanceof C
  • Is a value v that may be from another realm, an instance of a class C in the current realm? That can’t always be checked, but some checks work:
    • Array.isArray(v)
    • typeof v === 'function'
  • If the class can come from another realm and the value may not come from the same realm, then we can’t perform an instance check.

Proposal

The goal of this proposal is to mostly replace typeof, instanceof, and Array.isArray() with the following mechanisms:

  • Symbol.typeMarker lets us assign cross-realm symbols as type markers for classes.
    • All built-in classes get type markers.
  • hasType(value, type) supports:
    • Checking if value has a primitive type: type is 'string', 'null', etc.
    • Checking if value is an instance of a class type.
    • Cross-realm instance checks via Symbol.typeMarker.
  • Two helper functions:
    • Object.isObject()
    • Value.isPrimitive()

These are my first thoughts. I’m not attached to names nor many of the details.

Open questions

  • Can hasType(v, Function) replace typeof v === 'function'?

ECMAScript Pattern Matching

  • Should the custom matchers proposed by ECMAScript Pattern Matching support Symbol.typeMarker?
//========== Symbol.typeMarker ==========
Object.defineProperties(Symbol, {
typeMarker: {
value: Symbol.for('Symbol.typeMarker'),
writable: false,
enumerable: false,
configurable: false,
},
});
const builtinClasses = [
globalThis.AggregateError,
globalThis.Array,
globalThis.ArrayBuffer,
globalThis.BigInt,
globalThis.BigInt64Array,
globalThis.BigUint64Array,
globalThis.Boolean,
globalThis.DataView,
globalThis.Date,
globalThis.Error,
globalThis.EvalError,
globalThis.FinalizationRegistry,
globalThis.Float32Array,
globalThis.Float64Array,
globalThis.Function,
globalThis.Int8Array,
globalThis.Int16Array,
globalThis.Int32Array,
globalThis.Map,
globalThis.Number,
globalThis.Object,
globalThis.Promise,
// Doesn’t really have instances: globalThis.Proxy,
globalThis.RangeError,
globalThis.ReferenceError,
globalThis.RegExp,
globalThis.Set,
globalThis.SharedArrayBuffer,
globalThis.String,
globalThis.Symbol,
globalThis.SyntaxError,
globalThis.TypeError,
globalThis.Uint8Array,
globalThis.Uint8ClampedArray,
globalThis.Uint16Array,
globalThis.Uint32Array,
globalThis.URIError,
globalThis.WeakMap,
globalThis.WeakRef,
globalThis.WeakSet,
];
for (const builtinClass of builtinClasses) {
const sym = Symbol.for('es.tc39.' + builtinClass.name);
defineProp(builtinClass, Symbol.typeMarker, sym);
defineProp(builtinClass.prototype, sym, true);
}
//========== hasType() ==========
function hasType(value, type) {
switch (type) {
case 'undefined':
case 'boolean':
case 'number':
case 'bigint':
case 'string':
case 'symbol':
return typeof value === type;
case 'null':
return value === null;
default: {
if (typeof type === 'string') {
throw new TypeError('Unknown primitive type: ' + JSON.stringify(type));
}
if (Object.hasOwn(type, Symbol.typeMarker)) {
return type[Symbol.typeMarker] in value;
}
if (typeof type === 'function' && Object.hasOwn(type, 'prototype')) {
return isPrototypeOf(type.prototype, value);
}
return false;
}
}
}
defineProp(globalThis, 'hasType', hasType);
//========== Object.isObject(), Value.isPrimitive() ==========
function isObject(value) {
return value !== null && (typeof value === 'object' || typeof value === 'function');
}
function isPrimitive(value) {
return !isObject(value);
}
defineProp(globalThis, 'Value', Object.create(null));
defineProp(globalThis.Value, 'isPrimitive', isPrimitive);
defineProp(globalThis.Object, 'isObject', isObject);
//========== Helpers ==========
function defineProp(obj, key, value) {
Object.defineProperty(
obj, key, {
value,
writable: true,
enumerable: false,
configurable: true,
}
);
}
const uncurryThis = (theFunc) => Function.prototype.call.bind(theFunc);
const isPrototypeOf = uncurryThis(Object.prototype.isPrototypeOf);
import * as assert from 'node:assert/strict';
import './hastype.mjs';
// node --experimental-vm-modules hastype_test.mjs
//========== Tests ==========
//----- Primive types -----
assert.equal(
hasType(undefined, 'undefined'), true
);
assert.equal(
hasType(null, 'null'), true
);
assert.equal(
hasType(123, 'number'), true
);
//----- Builtin object types -----
assert.equal(
hasType([], Array), true
);
assert.equal(
hasType([], Object), true
);
//----- Local realm instance checks for user classes -----
class MyLocalClass {}
assert.equal(
hasType(new MyLocalClass(), MyLocalClass), true
);
assert.equal(
hasType({}, MyLocalClass), false
);
//----- Cross-realm instance checks -----
//····· Setting up the foreign realm ·····
import * as vm from 'node:vm';
import * as fs from 'node:fs';
async function linker(specifier, referencingModule) {
const src = fs.readFileSync(specifier, {encoding: 'utf-8'});
return new vm.SourceTextModule(
src,
{
context: referencingModule.context,
}
);
}
const foreignContext = vm.createContext({
print: console.log
});
// Experimental API: https://nodejs.org/api/vm.html#class-vmmodule
const foreignModule = new vm.SourceTextModule(
`
import './hastype.mjs';
export const foreignArray = [];
const CrossRealmClassMarker = Symbol.for('com.2ality.MyCrossRealmClass');
class CrossRealmClass {
static [Symbol.typeMarker] = CrossRealmClassMarker;
get [CrossRealmClassMarker]() {}
}
export const foreignInstance = new CrossRealmClass();
`,
{
context: foreignContext,
}
);
await foreignModule.link(linker);
await foreignModule.evaluate();
const {foreignArray, foreignInstance} = foreignModule.namespace;
//····· Foreign Array ·····
assert.equal(
foreignArray instanceof Array, false
);
assert.equal(
Array.isArray(foreignArray), true
);
assert.equal(
hasType(foreignArray, Array), true
);
//····· Foreign instance ·····
const CrossRealmClassMarker = Symbol.for('com.2ality.MyCrossRealmClass');
class CrossRealmClass {
static [Symbol.typeMarker] = CrossRealmClassMarker;
// Create property CrossRealmClass.prototype[CrossRealmClassMarker]
// (value doesn’t matter, only key):
[CrossRealmClassMarker]() {}
}
assert.equal(
hasType(new CrossRealmClass(), CrossRealmClass), true
);
assert.equal(
hasType(foreignInstance, CrossRealmClass), true
);
assert.equal(
hasType({}, CrossRealmClass), false
);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment