Sorry for the verbose title. I have a class MutateMe
passed into a factory called FilterFactory
by a decorator Decorator
.
export const Decorator = (options?: DecoratorOptions) => <T extends Constructor>(target: T) => {
new FilterFactory(target, options);
}
Within this factory, I'm copying over the methods onto the target
class and setting its metadata.
export class FilterFactory {
constructor(protected target: any, options: DecoratorOptions) {
// Getting the reference to the class from where I want to copy over methods with their own metadata
const routesController = FilterController;
// The class itself consists of a prefix that must be prepended to all its member methods' metadata.
const prefixRoute = getControllerPrefix(routesController);
console.log("For each key (member name)")
Reflect.ownKeys(routesController.prototype).forEach(
(property) => {
// Ignore the primitive class methods
if (!['constructor', 'toString', 'length'].includes(property.toString())) {
// Copy the methods over to the `target`
Object.defineProperty(
target.prototype,
property,
Object.getOwnPropertyDescriptor(
routesController.prototype,
property
)
)
// Prepends class metadata `filter` to each route method's metadata
patchRoutes(target.prototype[property], prefixRoute)
// NOTE: An alternative to prototype property assignment (Doesn't work either)
// target.prototype[property] = routesController.prototype[property]
console.log(Reflect.getOwnMetadata(PATH_METADATA, target.prototype[property]))
}
})
}
}
The patchRoutes
function is like so:
const patchRoutes = <K, T extends string, P>(patchee: any, patches: (T | T[] | ((...args: P[]) => (T | T[]))), ...args: P[]) => {
const existingPath = Reflect.getOwnMetadata(PATH_METADATA, patchee)
if (patches instanceof Function) { patches = patches(...args) }
if (!Array.isArray(patches)) patches = [patches]
Reflect.defineMetadata(PATH_METADATA, (existingPath === "/" ? [...patches] : [...patches, existingPath]).join("/"), patchee)
const createResetCallback = (resetValue, resetTarget) => () =>
Reflect.defineMetadata(PATH_METADATA, resetValue, resetTarget)
return createResetCallback(existingPath, patchee)
}
It returns a reset
callback to reset the patched metadata.
Now, when I decorate more than one classes with this decorator, I can see a duplication of patching.
For example, patching once would give me foo/filter/...
and for the second call, it'd give me bar/filter/filter/...
.
I wanted to see if it was the problem of copying methods over improperly, so, I tried patching the base class, copy over the patched methods and reset the metadata of the base class:
const propertyResetCb = patchRoutes(routesController.prototype[property], prefixRoute)
...
// Assigning the property now to the target
...
// Calling the reset callback
propertyResetCb()
However, this seems to reset the property of all the decorators that I've made.
This leads me to believe that it's using a singular prototype reference for the copied over methods. I'd wish to copy them free of reference (clone if you will) so that I can independently set their metadata.
Also, I'd prefer if I didn't have to modify the patchRoutes
to factor in the duplication because, in the end, I'd like to do more modifications to their individual metadata separately.
Thanks :)
Answer by @Mirco S. solved my question. Just had to add this little bit of metadata copying logic as well.
Reflect.defineMetadata(
PATH_METADATA,
Reflect.getOwnMetadata(PATH_METADATA, oldPropertyDescriptor.value),
newPropertyDescriptor.value
)
This is probably because the property value
in the property descriptor is always the same function. There is no generic deep copy function that works on all kind of objects but for functions you could try something like this:
// clone the propertyDescriptor to not temper with the original.
const newPropertyDescriptor = {...Object.getOwnPropertyDescriptor(
routesController.prototype,
property
)}
if(typeof newPropertyDescriptor.value === "function") {
const routesControllerFunction = newPropertyDescriptor.value;
// wrap the original function so that Reflect.defineMetadata gets applied to the
// newly created function instead of to the prototype function of FilterController
newPropertyDescriptor.value = (...args: any[]) => routesControllerFunction(...args);
}
Object.defineProperty(
target.prototype,
property,
newPropertyDescriptor
)
If you need to clone more than functions, you have to add more cases and properly clone them. Be careful though, if you copy functions like this, you temper around with bindings and might have to add logic to maintain a proper this
inside the decorated classes.
Edit: A little sidenote about decorators. I love decorators myself, but after several long years they are still in stage 2. The current implementation of decorators in typescript is inspired by the legacy proposal from 2014 which is not conform anymore with the current proposal. The newest proposal is WIP and as far as im aware there is no transpilation for them available (March, 2021). There are some breaking changes in the newest proposal, so be aware you might have to update your decorators in the future. Though, everything you are able to do with legacy decorators should be doable with the newest proposal. There is also the possibility we get yet another proposal....