How to get child classes which implement a certain base class using reflection in Type Script?

后端 未结 3 1768
长情又很酷
长情又很酷 2021-01-05 23:37

Can we use reflection in Type Script just like C# to get the list of classes which implement a certain base class?

For example, let say Snake and Horse implement the

3条回答
  •  南笙
    南笙 (楼主)
    2021-01-06 00:28

    If you are willing to take a dependency on a feature which is likely to change fairly soon, as it is based on an obsolete specification, and if you are willing to use only classes and not interfaces, you can accomplish this using a decorator.

    Here is an example:

    hierarchy-tracked.ts

    export default function hierarchyTracked(target: new (...args: any[]) => object) {
      for (const proto of walkPrototypeChain(target)) {
        if (!Object.hasOwnProperty.call(proto, 'extendedBy')) {
          const extendedBy: typeof Function.extendedBy = [];
          Object.defineProperty(proto, 'extendedBy', {
            get: () => extendedBy
          });
        }
        // ! is used to suppress a strictNullChecks error on optional.
        // This is OK since we know it is now defined.
        proto.extendedBy!.push(target);
      }
    }
    
    declare global {
      interface Function {
        // Declared as optional because not all classes are extended.
        extendedBy?: Array object>;
      }
    }
    
    function* walkPrototypeChain(target: new (...args: any[]) => object) {
      let proto = Reflect.getPrototypeOf(target);
      while (proto && proto !== Object) {
        yield proto;
        proto = Reflect.getPrototypeOf(proto);
      }
    }
    

    animals.ts

    import hierarchyTracked from './hierarachy-tracked';
    
    export class Animal {
      alive = true;
      static get slayable() {return true;}
      static extinct = false;
    }
    
    @hierarchyTracked export class Snake extends Animal {
      isEctotherm = true;
    }
    
    @hierarchyTracked export class Cobra extends Snake {
      isDeadly = true;
    }
    
    @hierarchyTracked export class Horse extends Animal {
      isEndotherm = true;
    }
    // logs
    Animal.extendedBy && Animal.extendedBy.map(Taxon => Taxon.name)
      .forEach(name => {
        console.log(name);
      });
    // Snake
    // Cobra
    // Horse
    
    Snake.extendedBy && Snake.extendedBy.map(Taxon => Taxon.name)
      .forEach(name => {
        console.log(name);
      });
    // Cobra
    

    There is no need to resort to global state and it is actually quite tidy and explicit.

    This also works with Babel 7 if you are not using TypeScript. (note that the same caveats regarding decorator usage mentioned above still apply)

    Of course this is trivial to write manually if you do not want to rely on decorators:

    import trackHierarchy from './hierarachy-tracked';
    
    export class Animal { }
    
    class Snake extends Animal { ... }
    trackHierarchy(Snake);
    
    export {Snake};
    

    Back to your example code above, it is easily achieved.

    It goes from

    var childTypes = assembly.GetTypes().Where(_ => _.IsSubclassOf(typeof(Animal)));
    

    to simply

    const childClasses = Animal.extendedBy || [];
    

    A Word of Warning

    If you find yourself wanting to write code like this, you should take a step back and make sure you actually know JavaScript. This sort of pattern, and indeed your example use case, usually indicates that someone has come to the language with a classical mindset, noticed ES 2015 classes, and began to think they are related to classes in traditional languages.

    ES classes could not be less like C#, C++, Java, or Scala classes.

    First and foremost: Classes in JavaScript are not types.

    Classes in JavaScript are values.

    Their declaration form is fundamentally just a syntactic sugar over prototypes. The pattern you are trying to achieve suggests you may not understand this well. In particular, it suggests that you may think that they are special.

提交回复
热议问题