UNPKG

@oazmi/kitchensink

Version:

a collection of personal utility functions

766 lines (765 loc) 31.2 kB
/** utility functions for common object structures and `Object` manipulation. * * @module */ import "./_dnt.polyfills.js"; import { array_isArray, object_defineProperty, object_getOwnPropertyDescriptor, object_getOwnPropertyNames, object_getOwnPropertySymbols, object_getPrototypeOf } from "./alias.js"; import { resolveRange } from "./array1d.js"; import { max } from "./numericmethods.js"; /** get an equivalent rectangle where the `height` and `width` are positive. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const * my_rect: Rect = { x: -20, y: 100, width: 50, height: -30 }, * my_abs_rect = positiveRect(my_rect) * * assertEquals(my_abs_rect, { * x: -20, * y: 70, * width: 50, * height: 30, * }) * ``` */ export const positiveRect = (r) => { let { x, y, width, height } = r; if (width < 0) { width *= -1; // width is now positive x -= width; // x has been moved further to the left } if (height < 0) { height *= -1; // height is now positive y -= height; // y has been moved further to the top } return { x, y, width, height }; }; /** get the constructor of a class's instance. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * class K { constructor(public value: any) { } } * * const a = new K(1) * const b = new (constructorOf(a))(2) // equivalent to `const b = new K(2)` * * a satisfies K * b satisfies K * assertEquals(a !== b, true) * ``` */ export const constructorOf = (class_instance) => { return object_getPrototypeOf(class_instance).constructor; }; /** use the constructor of a class's instance to construct a new instance. * * this is useful for avoiding pollution of code with `new` keyword along with some wonky placement of braces to make your code work. * * @example * ```ts * class K { * value: number * * constructor(value1: number, value2: number) { * this.value = value1 + value2 * } * } * * const a = new K(1, 1) * const b = constructFrom(a, 2, 2) // equivalent to `const b = new K(2, 2)` * * // vanilla way of constructing `const c = new K(3, 3)` using `a` * const c = new (Object.getPrototypeOf(a).constructor)(3, 3) * * a satisfies K * b satisfies K * c satisfies K * ``` */ export const constructFrom = (class_instance, ...args) => { return new (constructorOf(class_instance))(...args); }; /** get the prototype object of a class. * * this is useful when you want to access bound-methods of an instance of a class, such as the ones declared as: * * ```ts ignore * class X { * methodOfProto() { } * } * ``` * * these bound methods are not available via destructure of an instance, because they then lose their `this` context. * the only functions that can be destructured without losing their `this` context are the ones declared via assignment: * * ```ts ignore * class X { * fn = () => { } * fn2 = function () { } * } * ``` * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const * array_proto = prototypeOfClass(Array<number>), * arr = [1, 2, 3, 4, 5] * * array_proto.push.call(arr, 6) * assertEquals(arr, [1, 2, 3, 4, 5, 6]) * * const slow_push_to_arr = (...values: number[]) => (arr.push(...values)) * // the following declaration is more performant than `slow_push_to_arr`, * // and it also has lower memory footprint. * const fast_push_to_arr = array_proto.push.bind(arr) * * slow_push_to_arr(7, 8) // sloww & bigg * fast_push_to_arr(9, 10) // quicc & smol * assertEquals(arr, [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]) * ``` */ export const prototypeOfClass = (cls) => { return cls.prototype; }; /** get the prototype chain of an object, with optional slicing options. * * @param obj the object whose prototype chain is to be found. * @param config optional configuration for slicing the full prototype chain. * @returns the sliced prototype chain of the given object. * * @example * ```ts * import { assertEquals, assertThrows } from "jsr:@std/assert" * * // aliasing our functions for brevity * const * fn = prototypeChainOfObject, * eq = assertEquals * * class A extends Array { } * class B extends A { } * class C extends B { } * class D extends C { } * * const * a = new A(0), * b = new B(0), * c = new C(0), * d = new D(0) * * eq(fn(d), [D.prototype, C.prototype, B.prototype, A.prototype, Array.prototype, Object.prototype, null]) * eq(fn(b), [B.prototype, A.prototype, Array.prototype, Object.prototype, null]) * * // slicing the prototype chain, starting from index 2 till the end * eq(fn(d, { start: 2 }), [B.prototype, A.prototype, Array.prototype, Object.prototype, null]) * * // slicing using a negative index * eq(fn(d, { start: -2 }), [Object.prototype, null]) * * // slicing using an object * eq(fn(d, { start: B.prototype }), [B.prototype, A.prototype, Array.prototype, Object.prototype, null]) * * // when the slicing object is not found, the start index will be assumed to be `0` (default value) * eq(fn(d, { start: Set.prototype }), [D.prototype, C.prototype, B.prototype, A.prototype, Array.prototype, Object.prototype, null]) * * // slicing between the `start` index (inclusive) and the end index * eq(fn(d, { start: 2, end: 6 }), [B.prototype, A.prototype, Array.prototype, Object.prototype]) * eq(fn(d, { start: 2, end: -1 }), [B.prototype, A.prototype, Array.prototype, Object.prototype]) * eq(fn(d, { start: 2, end: null }), [B.prototype, A.prototype, Array.prototype, Object.prototype]) * * // if the end index is not found, the slicing will occur till the end * eq(fn(d, { end: Set.prototype}), [D.prototype, C.prototype, B.prototype, A.prototype, Array.prototype, Object.prototype, null]) * * // slicing using a `delta` argument will let you define how many elements you wish to: * // - traverse forward from the `start` index * // - traverse backwards from the `end` index * eq(fn(d, { start: 2, delta: 3 }), [B.prototype, A.prototype, Array.prototype]) * eq(fn(d, { end: -2, delta: 2 }), [A.prototype, Array.prototype]) * eq(fn(d, { end: null, delta: 4 }), [B.prototype, A.prototype, Array.prototype, Object.prototype]) * * eq(fn(d, { start: 1, delta: 1 }), fn(c, { start: 0, delta: 1 })) * eq(fn(c, { start: 1, delta: 1 }), fn(b, { start: 0, delta: 1 })) * eq(fn(b, { start: 1, delta: 1 }), fn(a, { start: 0, delta: 1 })) * eq(fn(a, { start: 1, delta: 1 }), fn([], { start: 0, delta: 1 })) * eq(fn([], { start: 1, delta: 1 }), fn({}, { start: 0, delta: 1 })) * * // you may also traverse through the inheritance chain of a class, but it will be a good idea to set your `end` point to `Function.prototype`, * // since all class objects are effectively functions (i.e. their common ancestral prototype if `Function.prototype`). * eq(fn(D, { start: 0, end: Function.prototype }), [C, B, A, Array]) * eq(fn(D), [C, B, A, Array, Function.prototype, Object.prototype, null]) * eq(fn(class {}), [Function.prototype, Object.prototype, null]) * eq(fn(class extends Object {}), [Object, Function.prototype, Object.prototype, null]) * eq(fn(class extends Map {}), [Map, Function.prototype, Object.prototype, null]) * * // you cannot acquire the prototype chain of the `null` object * assertThrows(() => { fn(null) }) * ``` */ export function prototypeChainOfObject(obj, config = {}) { let { start, end, delta } = config; const full_chain = []; // collecting the full prototype chain, until `obj` becomes `null`, which is always the last thing in a prototype chain. while ((obj = object_getPrototypeOf(obj))) { full_chain.push(obj); } full_chain.push(null); const full_chain_length = full_chain.length; // NOTE: we not only check if `start` and `end` are of type `"object"`, but also if they're of type `"function"`. // this is because `typeof Function.prototype === "function"`, and not `"object"`. // and as it happens to be the case, I do use `Function.prototype` as the end point in my {@link subclassThroughComposition} function. if (isComplex(start)) { start = max(0, full_chain.indexOf(start)); } if (isComplex(end)) { const end_index = full_chain.indexOf(end); end = end_index < 0 ? undefined : end_index; } // both `start` and `end` are either numbers or undefined now. if ((delta !== undefined) && ((start ?? end) !== undefined)) { // in here, `delta` is defined and one of either `start` or `end` is undefined if (start !== undefined) { end = start + delta; } else { start = end - delta; } } // now that any potential `delta` has been resolved into a `start` and `end`, // we only need to resolve possible negative indexes and then slice the prototype chain [start, end] = resolveRange(start, end, full_chain_length); return full_chain.slice(start, end); } /** get an object's list of **owned** keys (string keys and symbol keys). * * > [!note] * > **owned** keys of an object are not the same as just _any_ key of the object. * > an owned key is one that it **directly** owned by the object, not owned through inheritance, such as the class methods. * > more precisely, in javascript, which ever member of an object that we call a _property_, is an **owned** key. * * if you wish to acquire **all remaining** inherited keys of an object, you will want use {@link getInheritedPropertyKeys}. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const * symbol_h = Symbol("symbol h"), * symbol_i = Symbol("symbol i"), * symbol_j = Symbol("symbol j") * * class A { * a = { v: 1 } * b = { v: 2 } * c: { v: number } * d() { return { v: 4 } } * e() { return { v: 5 } } * f = () => { return { v: 6 } } * g: () => ({ v: number }) * [symbol_h]() { return { v: 8 } } * [symbol_i] = () => { return { v: 9 } } * * constructor() { * this.c = { v: 3 } * this.g = () => { return { v: 7 } } * } * } * * class B extends A { * override a = { v: 11 } * override e() { return { v: 15 } } * //@ts-ignore: typescript does not permit defining a method over the name of an existing property * override g() { return { v: 17 } } * [symbol_j] = () => { return { v: 20 } } * * constructor() { super() } * } * * const * a = new A(), * b = new B() * * assertEquals(b.a, { v: 11 }) * assertEquals(b.b, { v: 2 }) * assertEquals(b.c, { v: 3 }) * assertEquals(b.d(), { v: 4 }) * assertEquals(b.e(), { v: 15 }) * assertEquals(b.f(), { v: 6 }) * assertEquals(b.g(), { v: 7 }) // notice that the overridden method is not called, and the property is called instead * assertEquals(Object.getPrototypeOf(b).g(), { v: 17 }) * assertEquals(b[symbol_h](), { v: 8 }) * assertEquals(b[symbol_i](), { v: 9 }) * assertEquals(b[symbol_j](), { v: 20 }) * * assertEquals( * new Set(getOwnPropertyKeys(a)), * new Set(["a", "b", "c", "f", "g", symbol_i]), * ) * assertEquals( * new Set(getOwnPropertyKeys(Object.getPrototypeOf(a))), * new Set(["constructor", "d", "e", symbol_h]), * ) * assertEquals( * new Set(getOwnPropertyKeys(b)), * new Set(["a", "b", "c", "f", "g", symbol_i, symbol_j]), * ) * assertEquals( * new Set(getOwnPropertyKeys(Object.getPrototypeOf(b))), * new Set(["constructor", "e", "g"]), * ) * ``` */ export const getOwnPropertyKeys = (obj) => { return [ ...object_getOwnPropertyNames(obj), ...object_getOwnPropertySymbols(obj), ]; }; /** get all **inherited** list of keys (string keys and symbol keys) of an object, up till a certain `depth`. * * directly owned keys will not be returned. * for that, you should use the {@link getOwnPropertyKeys} function. * * the optional `depth` parameter lets you control how deep you'd like to go collecting the inherited keys. * * @param obj the object whose inherited keys are to be listed. * @param depth the inheritance depth until which the function will accumulate keys for. * - if an object `A` is provided as the `depth`, * then all inherited keys up until `A` is reached in the inheritance chain will be collected, but not including the keys of `A`. * - if a number `N` is provided as the `depth`, * then the function will collect keys from `N` number of prototypes up the inheritance chain. * - a depth of `0` would imply no traversal. * - a depth of `1` would only traverse the first direct prototype of `obj` (i.e. `getOwnPropertyKeys(Object.getPrototypeOf(obj))`). * @defaultValue `Object.prototype` * @returns an array of keys that the object has inherited (string or symbolic keys). * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * const * symbol_h = Symbol("symbol h"), * symbol_i = Symbol("symbol i"), * symbol_j = Symbol("symbol j") * * class A { * a = { v: 1 } * b = { v: 2 } * c: { v: number } * d() { return { v: 4 } } * e() { return { v: 5 } } * f = () => { return { v: 6 } } * g: () => ({ v: number }) * [symbol_h]() { return { v: 8 } } * [symbol_i] = () => { return { v: 9 } } * * constructor() { * this.c = { v: 3 } * this.g = () => { return { v: 7 } } * } * } * * class B extends A { * override a = { v: 11 } * override e() { return { v: 15 } } * //@ts-ignore: typescript does not permit defining a method over the name of an existing property * override g() { return { v: 17 } } * [symbol_j] = () => { return { v: 20 } } * * constructor() { super() } * } * * const * a = new A(), * b = new B() * * assertEquals( * new Set(getInheritedPropertyKeys(a)), * new Set(["constructor", "d", "e", symbol_h]), * ) * * // below, notice how the inherited keys of `a` equal to its prototype's owned keys. * // this is because methods of instances of `A` are defined on the prototype (i.e. properties of the prototype, rather than the instances'). * assertEquals( * new Set(getInheritedPropertyKeys(a)), * new Set(getOwnPropertyKeys(A.prototype)), * ) * * // also notice that inherited keys of `A.prototype` comes out as empty here, * // even though it does techinally inherit members from its own prototype (which is `Object.prototype`). * // the reason is that by default, we do not go deeper than `Object.prototype` to look for more keys. * // this is because from an end-user's perspective, those keys are not useful. * assertEquals( * getInheritedPropertyKeys(A.prototype), * [], * ) * * assertEquals( * new Set(getInheritedPropertyKeys(b)), * new Set(["constructor", "e", "g", "d", symbol_h]), * ) * assertEquals( * new Set(getInheritedPropertyKeys(b)), * new Set([ * ...getOwnPropertyKeys(B.prototype), * ...getInheritedPropertyKeys(B.prototype), * ]), * ) * assertEquals( * new Set(getInheritedPropertyKeys(B.prototype)), * new Set(["constructor", "e", "d", symbol_h]), * ) * * // testing out various depth * assertEquals( * new Set(getInheritedPropertyKeys(a, 1)), * new Set(getOwnPropertyKeys(A.prototype)), * ) * assertEquals( * new Set(getInheritedPropertyKeys(b, 1)), * new Set(getOwnPropertyKeys(B.prototype)), * ) * assertEquals( * new Set(getInheritedPropertyKeys(b, A.prototype)), * new Set(getOwnPropertyKeys(B.prototype)), * ) * assertEquals( * new Set(getInheritedPropertyKeys(b, 2)), * new Set([ * ...getOwnPropertyKeys(B.prototype), * ...getOwnPropertyKeys(A.prototype), * ]), * ) * // below, we collect all inherited keys of `b`, including those that come from `Object.prototype`, * // which is the base ancestral prototype of all objects. * // to do that, we set the `depth` to `null`, which is the prototype of `Object.prototype`. * // the test below may fail in new versions of javascript, where * assertEquals( * new Set(getInheritedPropertyKeys(b, null)), * new Set([ * "constructor", "e", "g", "d", symbol_h, * "__defineGetter__", "__defineSetter__", "hasOwnProperty", "__lookupGetter__", "__lookupSetter__", * "isPrototypeOf", "propertyIsEnumerable", "toString", "valueOf", "toLocaleString", * ]), * ) * ``` */ export const getInheritedPropertyKeys = (obj, depth = prototypeOfClass(Object)) => { // weird but expected facts: // - `Object.prototype !== Object.getPrototypeOf(Object)` // - `Object.prototype === Object.getPrototypeOf({})` // - `null === Object.getPrototypeOf(Object.prototype)` // - `you === "ugly"` const prototype_chain = prototypeChainOfObject(obj, { start: 0, end: depth }), inherited_keys = prototype_chain .map((prototype) => (getOwnPropertyKeys(prototype))) .flat(1); return [...(new Set(inherited_keys))]; }; /** get all **owned** getter property keys of an object `obj` (string keys and symbol keys). * * TODO: in the future, consider creating a `getInheritedGetterKeys` function with the same signature as `getInheritedPropertyKeys`. * * > [!note] * > inherited getter property keys will not be included. * > only directly defined getter property keys (aka owned keys) will be listed. */ export const getOwnGetterKeys = (obj) => { return getOwnPropertyKeys(obj).filter((key) => ("get" in object_getOwnPropertyDescriptor(obj, key))); }; /** get all **owned** setter property keys of an object `obj` (string keys and symbol keys). * * TODO: in the future, consider creating a `getInheritedSetterKeys` function with the same signature as `getInheritedPropertyKeys`. * * > [!note] * > inherited setter property keys will not be included. * > only directly defined setter property keys (aka owned keys) will be listed. */ export const getOwnSetterKeys = (obj) => { return getOwnPropertyKeys(obj).filter((key) => ("set" in object_getOwnPropertyDescriptor(obj, key))); }; /** create a new object that mirrors that functionality of an existing object `obj`, through composition. * * @param obj the object that is to be mimicked and composed. * @param config control how the mirroring composition should behave. * refer to the docs of {@link MirrorObjectThroughCompositionConfig} for more details. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * type TypeofB = Array<number> & { getLength(): number } * type TypeofC = TypeofB & { countZeros(): number } * * const a = Array.prototype * const b = { * getLength(): number { return this.length } * } as TypeofB * const c = { * countZeros(): number { return [...this].filter((value) => (value === 0)).length } * } as TypeofC * * Object.setPrototypeOf(b, a) * Object.setPrototypeOf(c, b) * * // below, we create an object `d` that mirrors the methods and protperties of `c`, but does not actually inherit it. * const d = mirrorObjectThroughComposition(c, { * baseKey: "_super", * propertyKeys: ["length"], * }) * * // notice that `d` does not inherit `c` as its prototype, despite being able to utilize its methods and properties. * assertEquals(Object.getPrototypeOf(d), Object.prototype, "`d` does not inherit `c`") * * d.push(0, 0, 0, 0, 1) * assertEquals([...d], [0, 0, 0, 0, 1], "`d` also mirrors symbolic keys (such as iterators)") * assertEquals([...c], [0, 0, 0, 0, 1], "mutations made to `d` are applied to `c`") * assertEquals(d._super, c, "`c` is accessible via the `baseKey` (\"_super\") property") * assertEquals(d.length, 5) * assertEquals(c.length, 5) * assertEquals(d.getLength(), 5) * assertEquals(d.countZeros(), 4) * d.splice(0, 3) * assertEquals(d.countZeros(), 1) * assertEquals(c.countZeros(), 1) * * // you may even hot-swap the object that is being composed inside of `d`. * const e = [1, 2, 3, 4, 5, 6, 7, 8, 9] * Object.setPrototypeOf(e, c) * d._super = e as TypeofC * assertEquals(d.length, 9) * assertEquals(d.getLength(), 9) * assertEquals(d.countZeros(), 0) * * // it is also possible for you to provide an existing `target` object onto which the mirroring should occur. * // moreover, you can ignore specific keys which you would like not to be mirrored using the `ignoreKeys` option. * class F { * methodF() { return "press f for your fallen comrades" } * // to stop typescript from complaining, we declare the instance methods and properties that will be mirrored. * declare _super: typeof c * declare length: number * declare getLength: undefined * declare countZeros: () => number * } * mirrorObjectThroughComposition(c, { * target: F.prototype, * baseKey: "_super", * propertyKeys: ["length"], * ignoreKeys: ["getLength"], * }) * const f = new F() * assertEquals(f instanceof F, true) * assertEquals(f._super, c) * assertEquals(f.length, 2) * assertEquals(f.countZeros(), 1) * assertEquals(f.getLength, undefined) // the `getLength` is not mirrored because it was present in the `ignoreKeys` option * assertEquals(f.methodF(), "press f for your fallen comrades") * ``` */ export const mirrorObjectThroughComposition = (obj, config) => { const { baseKey, mirrorPrototype = true, propertyKeys = [], ignoreKeys = [], target = {} } = config, mirror_obj = target, property_keys = new Set(propertyKeys), ignore_keys = new Set(ignoreKeys), prototype_chain = isBoolean(mirrorPrototype) ? (mirrorPrototype ? prototypeChainOfObject(obj, { end: prototypeOfClass(Object) }) : []) : isArray(mirrorPrototype) ? mirrorPrototype : prototypeChainOfObject(obj, mirrorPrototype); prototype_chain.unshift(obj); mirror_obj[baseKey] = obj; // dynamically defining the methods composed from the base object `obj` for (const prototype of prototype_chain) { const prototype_keys = getOwnPropertyKeys(prototype); for (const key of prototype_keys) { if ((key in mirror_obj) || property_keys.has(key) || ignore_keys.has(key)) { continue; } const { value, ...description } = object_getOwnPropertyDescriptor(prototype, key); if (isFunction(value)) { // `value` is very likely to be a prototype-bound method. // so we create an appropriate proxying/wrapper method for it. object_defineProperty(mirror_obj, key, { ...description, value(...args) { // NOTE: the motivation behind returning `this[baseKey][key](...args)` instead of `obj[key](...args)` is that it would // allow us to later on dynamically replace the underlying object that is being mirrored, which is a necessity when // dealing with class-instance object mirroring that initially being defined on class's own prototype base constructor. return this[baseKey][key](...args); } }); } else { // `value` is either an instance-bound property or does not exist exist (i.e. there's a getter/setter method in its place instead) property_keys.add(key); } } } // dynamically defining the potentially transparent properties (i.e. later defined) of the composed object for (const key of property_keys) { // TODO: what if there's more to the description of the given key? we're certainly not preserving that information here unfortunately. object_defineProperty(mirror_obj, key, { get() { return this[baseKey][key]; }, set(new_value) { this[baseKey][key] = new_value; }, }); } return mirror_obj; }; /** create a subclass of the given `cls` class through composition rather than ES6 inheritance (aka `extends`). * the composed instance resides under the property `this._super`, and all instance methods of the super class are copied/mirrored in the output class. * * TODO: provide a motivation/justification when compositional-mirroring subclass might be necessary to use instead of ES6 `extends` inheritance. * * @param cls the class to subclass through composition. * @param property_keys specify additional property keys that exist within the instance of the class that need to be mirrored. * @returns a class whose instances fully mirror the provided `cls` class's instance, without actually inheriting it. * * @example * ```ts * import { assertEquals } from "jsr:@std/assert" * * class A<T> extends Array<T> { constructor(quantity: number) { super(quantity) } } * class B<T> extends subclassThroughComposition(A, { * class: {}, * instance: { propertyKeys: ["length"] }, * })<T> { } * * // `B` mirrors the constructor of `A`, as well as provide an additional `"_super"` property to access the enclosed (class which is `A`) * B satisfies { new(amount: number): B<any> } * B._super satisfies typeof A * // the following cannot be satisfied due to `A`'s restrictive constructor: `B satisfies { new(item1: string, item2: string, item3: string): B<string> }` * // even though `A`'s super class (`Array`) does permit that kind of constructor signature: * Array satisfies { new(item1: string, item2: string, item3: string): Array<string> } * * const my_array = new B<number>(5) * * // `my_array` encloses an instance of its "subclass" (which is an instance of class `A`) under the `"_super"` key. * assertEquals(my_array instanceof B, true) * assertEquals(my_array._super instanceof A, true) * my_array._super satisfies A<unknown> // this should have been narrowed to `satisfies A<number>`, but typing class extensions with generics is nearly impossible * * // `my_array` mirrors the properties and methods of the instances of the "subclass" that it encloses. * my_array.fill(1) * my_array.push(2) * my_array.push(3) * assertEquals(my_array.at(-1), 3) * assertEquals(my_array._super.at(-1), 3) * assertEquals(my_array.length, 7) * assertEquals(my_array._super.length, 7) * * // `my_array` mirrors the symbolic properties and methods as well. * assertEquals([...my_array], [1, 1, 1, 1, 1, 2, 3]) // the `Symbol.iterator` method is mirrored successfully * * // `my_array` when a method that returns `new this.constructor(...)` (such as `Array.prototype.splice`), then an instance of * // the enclosed subclass `A` will be created instead of `B` (had we had a regular inheritance via `class B extends A { }`). * const * splice_of_my_array = my_array.splice(0, 4, 0, 0), * slice_of_my_array = my_array.slice(3, 4) * assertEquals(splice_of_my_array instanceof A, true) * assertEquals(slice_of_my_array instanceof A, true) * assertEquals([...splice_of_my_array], [1, 1, 1, 1]) * assertEquals([...slice_of_my_array], [2]) * assertEquals([...my_array], [0, 0, 1, 2, 3]) * assertEquals([...my_array._super], [0, 0, 1, 2, 3]) * * // the class `B` also mirrors static properties and static methods of `A` (and its inherited ones). * // this means that any static method that creates a new instance via `new this(...)` will create an instance of `A` instead of `B`. * const * new_array_1 = B.from(["hello", "world"]), * new_array_2 = B.of("goodbye", "world") * assertEquals(new_array_1 instanceof A, true) * assertEquals(new_array_2 instanceof A, true) * assertEquals(new_array_1, A.from(["hello", "world"])) * assertEquals(new_array_2, A.of("goodbye", "world")) * ``` */ export const subclassThroughComposition = (cls, config = {}) => { const class_config = config.class ?? {}, instance_config = config.instance ?? {}, instance_base_key = instance_config.baseKey ?? "_super", class_ignore_keys = class_config.ignoreKeys ?? [], new_cls = class { constructor(...args) { const composite_instance = new cls(...args); // @ts-ignore: dynamic computed key assignment is not permitted in typescript this[instance_base_key] = composite_instance; } }, //as ConstructorOf<MirrorComposition<T, INSTANCE_KEY>, Args> & MirrorComposition<CLS, CLASS_KEY>, cls_prototype = prototypeOfClass(cls), new_cls_prototype = prototypeOfClass(new_cls); // mirroring the static class methods and static properties of `cls` onto `new_cls` mirrorObjectThroughComposition(cls, { // NOTE: all classes extend the `Function` constructor (i.e. if we had `class A {}`, then `Object.getPrototypeOf(A) === Function.prototype`). // but obviously, we don't want to mirror the `Function` methods within `cls` (aka the static methods of `Function`), // which is why we set our default class's config `mirrorPrototype.end` to `Function.prototype`. mirrorPrototype: { start: 0, end: prototypeOfClass(Function) }, baseKey: "_super", ...class_config, // the ignore list ensures that we don't overwrite existing properties of the `target` class. // (even though there are checks in place that would prevent overwriting existing keys, it's better to be explicit) ignoreKeys: [...class_ignore_keys, "length", "name", "prototype"], target: new_cls, }); // mirroring the instance methods and properties of `cls.prototype` onto `new_cls.prototype` mirrorObjectThroughComposition(cls_prototype, { baseKey: instance_base_key, ...instance_config, target: new_cls_prototype, }); return new_cls; }; /** monkey patch the prototype of a class. * * TODO: give usage examples and situations where this will be useful. */ export const monkeyPatchPrototypeOfClass = (cls, key, value) => { object_defineProperty(prototypeOfClass(cls), key, { value }); }; /** check if `obj` is either an object or function. */ export const isComplex = (obj) => { const obj_type = typeof obj; return obj_type === "object" || obj_type === "function"; }; /** check if `obj` is neither an object nor a function. */ export const isPrimitive = (obj) => { return !isComplex(obj); }; /** check if `obj` is a `function`. * * TODO: consider if it would be a good idea to include a generic parameter for the function's signature. * i.e.: `<FN extends Function = Function>(obj: any): obj is FN` */ export const isFunction = (obj) => { return typeof obj === "function"; }; /** check if `obj` is an `Object`. */ export const isObject = (obj) => { return typeof obj === "object"; }; /** check if `obj` is an `Array`. */ export const isArray = array_isArray; /** check if `obj` is a `Record`, which is any non-nullable object that isn't an array; * kind of like a dictionary. */ export const isRecord = (obj) => { return isObject(obj) && obj !== null && !isArray(obj); }; /** check if `obj` is a `string`. */ export const isString = (obj) => { return typeof obj === "string"; }; /** check if `obj` is a `number`. */ export const isNumber = (obj) => { return typeof obj === "number"; }; /** check if `obj` is a `bigint`. */ export const isBigint = (obj) => { return typeof obj === "bigint"; }; /** check if `obj` is either a `number` or a `bigint`. */ export const isNumeric = (obj) => { return typeof obj === "number" || typeof obj === "bigint"; }; /** check if `obj` is `boolean`. */ export const isBoolean = (obj) => { return typeof obj === "boolean"; }; /** check if `obj` is a `symbol`. */ export const isSymbol = (obj) => { return typeof obj === "symbol"; };