javascriptnode.jstypescriptenumerable

Why does enumerable: false not cascade to inherited classes in TypeScript?


If I flag a property as @enumerable(false) using TypeScript and the method below, the child classes that extend the parent class where this enumerable is flagged to false will have the property but it will be enumerable taking from this answer.

export {}

declare global {
    function enumerable(value: boolean): any;
}

const _global = (global /* node */ || window /* browser */) as any;

/**
 * @enumerable decorator that sets the enumerable property of a class field to false.
 * @param value true|false
 */
_global.enumerable = function(value: boolean): any {
    return function (target: any, propertyKey: string) {
        let descriptor = Object.getOwnPropertyDescriptor(target, propertyKey) || {};
        if (descriptor.enumerable != value) {
            descriptor.configurable = true;
            descriptor.writable = true;
            descriptor.enumerable = value;
            Object.defineProperty(target, propertyKey, descriptor)
        }
    };
}

Here's what my hierarchy looks like:

class BaseObject {
    @enumerable(false)
    public _metadata: any = {
        id: 'number',
        name: 'string'
    };
}

class ContainerObject extends BaseObject {
    // ...
}

class CanvasObject extends BaseObject {
    // ...
}

And here's what the value of the descriptor is at runtime:

var canvas = new CanvasObject();
console.log('Metadata Descriptor: ');
console.log(Object.getOwnPropertyDescriptor(canvas, '_metadata'));
Metadata Descriptor:
{ value: 
   { beanId: 'number',
     name: 'string' },
  writable: true,
  enumerable: true,
  configurable: true }

How can I ensure that this property is enumerable: false in the parent class and all subsequent inherited classes?


Solution

  • This is caused by the fact that a decorator is applied to class prototype on class declaration. Since _metadata is instance property (it's desugared to constructor() { this._metadata = ... }), enumerable decorator doesn't affect it.

    Here is an example of enumerable decorator that can be applied to both prototype (usually methods) and instance properties:

    function enumerable(value: boolean) {
      return (target: any, prop: string, descriptor?: PropertyDescriptor) => {
        if (descriptor) {
          // prototype property
          descriptor.enumerable = value;
        } else {
          // instance property
          let propSymbol = Symbol(prop);
    
          Object.defineProperty(target, prop, {
            configurable: true,
            enumerable: value,
            get() { return this[propSymbol] },
            set(value) { this[propSymbol] = value }
          });
        }
      };
    }
    

    Notice that in order to deal with _metadata = ... property initializer, descriptor should contain set accessor to catch property assignments.