I'm trying to make an @enumerable decorator that will expose properties defined via accessor methods.
A function to do this on instances of the class is fairly trivial:
// This works great when called in the class constructor like ```makeEnumerable(this, ['prop1', 'prop2'])```
const makeEnumerable = (what: any, props: string[]) => {
for (const property of props) {
const descriptor = Object.getOwnPropertyDescriptor(what.constructor.prototype, property);
if (descriptor) {
const modifiedDescriptor = Object.assign(descriptor, { enumerable: true });
Object.defineProperty(what, property, modifiedDescriptor);
}
}
};
However, it does not seem possible to turn this into a decorator, because it doesn't have the instance.
// Does not work for Object.keys, Object.getOwnPropertyNames or Object.entries
function enumerable (value: boolean = true): any {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor): any {
if (descriptor) {
Object.assign(descriptor, { enumerable: value });
}
};
}
The property does still enumerate in for (const x in y)
structures (strangely), but nowhere else - and worse, Object.entries
throws an error.
Here is an example using the functions above:
class MyClass {
#privateVal1: any;
#privateVal2: any;
constructor () {
makeEnumerable(this, ['b']);
}
@enumerable(true)
get a () {
return this.#privateVal1;
}
set a (val: any) {
this.#privateVal1 = val;
}
get b () {
return this.#privateVal2;
}
set b (val: any) {
this.#privateVal2 = val;
}
}
const enumerableA = new MyClass();
enumerableA.a = 5;
enumerableA.b = 6;
const keys = [];
for (const key in enumerableA) {
keys.push(key);
}
console.log({
'forin': keys, // ['a', 'b']
'keys': Object.keys(enumerableA), // ['b']
'keys(proto)': Object.keys(Object.getPrototypeOf(enumerableA)), // ['a']
'getOwnPropertyNames': Object.getOwnPropertyNames(enumerableA), // ['b']
'getOwnPropertyNames(proto)': Object.getOwnPropertyNames(Object.getPrototypeOf(enumerableA)), // ['constructor', 'a', 'b']
});
console.log({
'entries': Object.entries(enumerableA), // Error('Cannot read private member #privateVal1 from an object whose class did not declare it');
'entries(proto)': Object.entries(Object.getPrototypeOf(enumerableA)), // Error('Cannot read private member #privateVal1 from an object whose class did not declare it');
});
Is there any way to use a decorator to make an accessor method an enumerable property?
There's nothing strange in that, you'll have to understand Prototype vs Instance properties
makeEnumerable
sets enumerable descriptors on the instance.enumerable
decorator modifies prototype-level descriptors.You are expecting Object.keys(enumerableA)
to be ['a', 'b']
, like 'forin': keys
, but:
for...in
loop iterates over both own and inherited enumerable properties.Object.keys
returns only it's own enumerable properties.Check this MDN blog for more info. https://developer.mozilla.org/en-US/docs/Web/JavaScript/Enumerability_and_ownership_of_properties#querying_object_properties
for (const key in enumerableA)
['a', 'b']
for...in
loop iterates over both own and inherited enumerable properties.b
and a
are enumerable but on different level. b
is made enumerable by makeEnumerable
as instance property and a
prototype property made enumerable by the @enumerable
decorator.Object.keys(enumerableA)
['b']
Object.keys
lists only the own enumerable properties.b
is made an own enumerable property by makeEnumerable
function in constructor.a
is still on the prototype, so it is excluded.Object.keys(Object.getPrototypeOf(enumerableA))
['a']
@enumerable
decorator modifies the prototype-level descriptor for a
.b
is non-enumerable on prototype because makeEnumerable
function made enumerable on instance only.Object.getOwnPropertyNames(enumerableA)
['b']
b
is an own property on the instance.Object.getOwnPropertyNames(Object.getPrototypeOf(enumerableA))
['constructor', 'a', 'b']
constructor
, and b
are non-enumerable but exist on prototype.Object.entries
throws an errorObject.entries
access all the enumerable own properties.
Object.entries(enumerableA)
:
b
as it is enumerable property on instance. While accessing b
, the this
context of get b() {...}
is the instance MyClass { b: [Getter/Setter] }
.this
referes to the instance.Object.entries(Object.getPrototypeOf(enumerableA))
a
because a
is an enumerable property on the prototype.this
context for the get a(){...}
is the prototype object ({ a: [Getter/Setter] }
), not an instance of MyClass
You must understand how private properties are handled by typescript.
this
when a method is called. If this
is not same as own class
it throws an error.No, it is not possible to make instance properties enumerable directly using decorators in Typescript because property decorators in Typescript only have access to the class prototype
for instance members, not the instance itself.
instance properities
enumerable use makeEnumerable
function, as you used for b
.I hope, I've addressed all your issues. If anything else you'd like to clarify, feel free to ask. Happy learning!