Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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

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 base class Animal. Now I need to get classes which implement Animal. Similar to what we can do in C#:

C# equivalent Code:

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

Type Script classes:

class Animal {
}

class Snake extends Animal {
}

class Horse extends Animal {
}
like image 309
Baig Avatar asked Feb 23 '17 11:02

Baig


People also ask

How do I find the class of an object in TypeScript?

Use the name property on an object's constructor to get the class name of the object, e.g. const className = e1.constructor.name . The constructor property returns a reference to the constructor function that created the object. Copied!

How do I inherit a class in TypeScript?

To inherit a class, you use the extends keyword. For example the following Employee class inherits the Person class: class Employee extends Person { //.. } In this example, the Employee is a child class and the Person is the parent class.

Can you have classes in TypeScript?

TypeScript supports object-oriented programming features like classes, interfaces, etc. A class in terms of OOP is a blueprint for creating objects. A class encapsulates data for the object. Typescript gives built in support for this concept called class.

Does TypeScript support class?

TypeScript offers full support for the class keyword introduced in ES2015. As with other JavaScript language features, TypeScript adds type annotations and other syntax to allow you to express relationships between classes and other types.


1 Answers

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<new (...args: any[]) => 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.

like image 91
Aluan Haddad Avatar answered Sep 28 '22 08:09

Aluan Haddad