@typescript-package/wrap-property
Version:
A lightweight TypeScript package for wrapping object properties.
327 lines (321 loc) • 12.8 kB
JavaScript
/**
* @description The core abstraction class for wrapping properties.
* @export
* @abstract
* @class WrapPropertyCore
* @template {Record<PropertyKey, any>} O The type of the object.
* @template {keyof O} [K=keyof O] The key type of the property to wrap.
* @template {boolean} [A=boolean] The type of active property.
* @template {boolean} [F=boolean] The type of enabled property.
* @template {boolean} [C=boolean] The type of configurable property.
* @template {boolean} [E=boolean] The type of enumerable property.
* @template {WrappedPropertyDescriptor<O, K, A, C, E>} [D=WrappedPropertyDescriptor<O, K, A, C, E>] The type of descriptor constrained by the `WrappedPropertyDescriptor`.
* @template {object | undefined} [R=undefined] The type of controller that controls the wrapping behavior.
*/
class WrapPropertyCore {
/**
* @description Defaults for active.
* The active property indicates whether the callbacks `onGet` and `onSet` are active.
* @public
* @static
* @type {boolean}
*/
static active = true;
/**
* @description Defaults for configurable.
* @public
* @static
* @type {boolean}
*/
static configurable = true;
/**
* @description Defaults for enabled state of wrapped property.
* If `true`, the property stores the value in the private key.
* If `false`, the property does not store the value in the private key.
* @public
* @static
* @type {boolean}
*/
static enabled = true;
/**
* @description Defaults for enumerable.
* @public
* @static
* @type {boolean}
*/
static enumerable = false;
/**
* @description The controller that controls the wrapping behavior.
* @public
* @abstract
* @readonly
* @type {R}
*/
get controller() {
return undefined;
}
}
// Abstract.
/**
* @description The foundational class for wrapping properties.
* @export
* @abstract
* @class WrapPropertyBase
* @template {Record<PropertyKey, any>} O The type of the object.
* @template {keyof O} [K=keyof O] The key type of the property to wrap.
* @template {boolean} [A=boolean] The type of active property.
* @template {boolean} [F=boolean] The type of enabled property.
* @template {boolean} [C=boolean] The type of configurable property.
* @template {boolean} [E=boolean] The type of enumerable property.
* @template {WrappedPropertyDescriptor<O, K, C, E>} [D=WrappedPropertyDescriptor<O, K, C, E>] The type of descriptor constrained by the `WrappedPropertyDescriptor`.
* @template {PropertyControllerShape<O, K, A, C, E, D>} [R=PropertyControllerShape<O, K, A, C, E, D>] The type of controller that controls the wrapping behavior.
* @extends {WrapPropertyCore<T, O, K, C, E, D, R>}
*/
class WrapPropertyBase extends WrapPropertyCore {
/**
* @inheritdoc
* @public
* @readonly
* @type {R}
*/
get controller() {
return this.#controller;
}
/**
* @description The descriptor that handles actual wrapped property.
* @public
* @readonly
* @type {D}
*/
get descriptor() {
return this.controller
? this.controller.descriptor
: this.#descriptor;
}
/**
* @description The key of the property to wrap.
* @protected
* @readonly
* @type {K}
*/
get key() {
return this.#key;
}
/**
* @description The previous descriptor of the wrapped property.
* @protected
* @readonly
* @type {*}
*/
get previousDescriptor() {
return this.controller
? this.controller.descriptor.previousDescriptor
: this.descriptor.previousDescriptor;
}
/**
* @description The private key used to store the value of the property.
* @protected
* @readonly
* @type {PropertyKey}
*/
get privateKey() {
return (this.controller
? this.controller.descriptor.privateKey
: this.descriptor.privateKey);
}
/**
* @description The target object of the property.
* @protected
* @readonly
* @type {T}
*/
get object() {
return this.#object;
}
/**
* @description Privately stored controller that controls the wrapping behavior.
* @type {?R}
*/
#controller;
/**
* @description
* @type {D}
*/
#descriptor;
/**
* @description Privately stored key of property.
* @type {K}
*/
#key;
/**
* @description The target in which the property is wrapped.
* @type {O}
*/
#object;
/**
* Creates an instance of `WrapPropertyBase` child class.
* @constructor
* @param {O} object The target object or class to wrap the property of the given `key`.
* @param {K} key The key to wrap the property.
* @param {?D} [descriptor] The descriptor of the property to wrap.
* @param {?new (descriptor?: D) => R} [controller] The controller that controls the wrapping behavior.
*/
constructor(object, key, descriptor, controller) {
super();
// If the descriptor is not provided, create a default one.
descriptor = {
...{
active: WrapPropertyCore.active,
configurable: WrapPropertyCore.configurable,
enumerable: WrapPropertyCore.enumerable,
previousDescriptor: Object.getOwnPropertyDescriptor(object, key),
privateKey: `_${String(key)}`
},
...descriptor,
};
// If the controller is provided, create an instance of it.
controller
? this.#controller = new controller(descriptor)
: this.#descriptor = descriptor;
// The key to wrap.
this.#key = key;
// The target object to wrap the property on.
this.#object = object;
// Define the private property to store the value.
this.#hasPrivateProperty(object) === false && this.#definePrivateProperty(object, key);
}
/**
* @description Unwraps the property using previous descriptor.
* @public
* @returns {this}
*/
unwrap() {
const previousDescriptor = (this.#controller ? this.#controller.descriptor : this.#descriptor).previousDescriptor;
previousDescriptor &&
(Object.defineProperty(this.object, this.key, previousDescriptor),
this.privateKey && delete this.object[this.privateKey]);
return this;
}
/**
* @description Wraps the property with a private indicator.
* @protected
* @param {O} object The object to wrap the property on.
* @param {K} key The key of the property to wrap.
* @returns {this}
*/
wrap(object, key) {
// Wrap acts as controller.
const controller = this.#controller ? this.#controller : this;
// The getter for the property.
const get = controller.descriptor.get
? controller.descriptor.get
: function () {
if (controller.descriptor.enabled === false) {
return undefined;
}
// Set the this as the target object.
const o = this;
// Get the previous value from descriptor.
const previousValue = controller.descriptor.previousDescriptor
? controller.descriptor.previousDescriptor.get && typeof controller.descriptor.previousDescriptor.get === 'function'
? controller.descriptor.previousDescriptor.get.call(this)
: controller.descriptor.previousDescriptor.value
: undefined;
// Current descriptor.
return controller.descriptor.onGet
&& ((typeof controller.descriptor.active === 'boolean' && controller.descriptor.active)
|| (typeof controller.descriptor.active === 'object' && controller.descriptor.active.onGet === true))
? controller.descriptor.onGet.call(o, key, previousValue, o[controller.descriptor.privateKey], o)
: o[controller.descriptor.privateKey];
};
// The setter for the property.
const set = controller.descriptor.set
? controller.descriptor.set
: function (value) {
if (controller.descriptor.enabled === false) {
return undefined;
}
// Set the this as the target object.
const o = this;
// Get the previous value from previous descriptor or current value.
const previousValue = (o[controller.descriptor.privateKey] || controller.descriptor.previousDescriptor?.value);
// Perform previous descriptor.
controller.descriptor.previousDescriptor?.set && controller.descriptor.previousDescriptor.set.call(o, value);
// Set the private property value.
Object.assign(o, {
[controller.descriptor.privateKey]: controller.descriptor.onSet
&& ((typeof controller.descriptor.active === 'boolean' && controller.descriptor.active)
|| (typeof controller.descriptor.active === 'object' && controller.descriptor.active.onSet === true))
? controller.descriptor.onSet.call(o, value, previousValue, key, o)
: value
});
};
// Define property with the given key and options.
Object.defineProperty(object, key, {
configurable: controller.descriptor.configurable, // Whether the property can be deleted or changed.
enumerable: controller.descriptor.enumerable, // Whether the property is visible in enumerations.
get, // Getter for the property.
set, // Setter for the property.
});
return this;
}
/**
* @description Checks if the object has a private property.
* @param {O} object The object to check for the private property.
* @returns {boolean}
*/
#hasPrivateProperty(object) {
return Object.hasOwn(object, this.privateKey);
}
/**
* @description Defines the private property in the object.
* @param {O} object The object to define the private property on.
* @param {K} key The key of the property to define.
*/
#definePrivateProperty(object, key) {
Object.defineProperty(object, this.privateKey, {
configurable: true,
enumerable: false,
value: object[key] || new object.constructor()[key],
writable: true
});
}
}
// Abstract.
/**
* @description The concrete class for wrapping object properties.
* @export
* @class WrapProperty
* @template {object | (new () => any)} T The type of the target object or class to wrap the property of the given `key`.
* @template {Record<PropertyKey, any>} [O=(T extends new () => T ? ( T extends { prototype: infer P } ? P : never) : T)] The object type of captured `T`.
* @template {keyof O extends string | symbol ? keyof O : never} [K=keyof O extends string | symbol ? keyof O : never] The key type of the property to wrap.
* @template {boolean} [A=boolean] The type of active property.
* @template {boolean} [C=boolean] The type of configurable property.
* @template {boolean} [E=boolean] The type of enumerable property.
* @template {WrappedPropertyDescriptor<O, K, C, E>} [D=WrappedPropertyDescriptor<O, K, C, E>] The type of descriptor constrained by the `WrappedPropertyDescriptor`.
* @template {PropertyControllerShape<O, K, A, C, E, D>} [Controller=PropertyControllerShape<O, K, A, C, E, D>] The type of controller that controls the wrapping behavior.
* @extends {WrapPropertyBase<T, O, K, C, E, D>}
*/
class WrapProperty extends WrapPropertyBase {
/**
* Creates an instance of `WrapProperty`.
* @constructor
* @param {O} object The target object or class to wrap the property of the given `key`.
* @param {K} key The key of the property to wrap.
* @param {?D} [descriptor] The descriptor of the property to wrap.
* @param {?new (descriptor?: D) => Controller} [controller] The controller that controls the wrapping behavior.
*/
constructor(object, key, descriptor, controller) {
super(object, key, descriptor, controller);
// Define the property with the given key and options.
this.wrap(object, key);
}
}
/*
* Public API Surface of wrap-property
*/
/**
* Generated bundle index. Do not edit.
*/
export { WrapProperty, WrapPropertyBase, WrapPropertyCore };
//# sourceMappingURL=typescript-package-wrap-property.mjs.map