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
One way to do it would be to keep track of all the classes you create in one global array:
var allClasses = []
function register(c) {
allClasses.push(c);
}
register(class Animal {});
register(class Snake extends Animal {});
register(class Horse extends Animal {});
register(class Car {});
and when you need classes that inherit from particular class just filter out the ones you want:
Object.values(allClasses).filter((c) => Animal.isPrototypeOf(c))
This solution is not specific to Typescript, it'll work with pure JavaScript too.
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.
I had the need to deserialize a response from a SOAP web service. I first used an existing SOAP client to get an object from the returned XML and then used a JSON deserializer - the one from http://cloudmark.github.io/Json-Mapping/ - to deserialize this object into the actual types that I want.
Problem was that one of the returned properties was declared as a base type and could actually contain instances of a number of different sub-types.
I solved it by creating a class decorator that I apply to the derived types. This class decorator gets the base class and applies metadata to it, describing the derived class.
To understand the following code, it is important to know that the XML -> JSON parser adds a $type
property if the XML from the web service contains a type
attribute which indicates that polymorphism is in play.
The actual metadata that is applied to the base type is the type name from the XML and the constructor of the derived type.
Meta data registration and class decorator:
export interface IJsonPolymorphism {
name: string;
clazz: {new(): any};
}
export function RegisterForPolymorphism(name: string) {
return (target) => {
let metadata = getJsonPolymorphism(target.__proto__);
if (!metadata) {
metadata = [];
}
metadata.push({name: name, clazz: target});
target.__proto__ = Reflect.metadata(jsonPolymorphismMetadataKey, metadata)(target.__proto__);
return target;
};
}
export function getJsonPolymorphism(target: any): IJsonPolymorphism[] {
return Reflect.getMetadata(jsonPolymorphismMetadataKey, target);
}
Usage:
// The base class
export class PropertyBase { /*...*/ }
// The derived classes
//
@RegisterForPolymorphism("b:PicklistProperty")
export class PicklistProperty extends PropertyBase { /*...*/ }
@RegisterForPolymorphism("b:TextProperty")
export class TextProperty extends PropertyBase { /*...*/ }
The strings that are passed to the class decorator are the values of the type
attribute in the XML response from the web service.
The code of the deserializer makes use of it like this:
if (jsonObject["$type"]) {
const polymorphism = getJsonPolymorphism(clazz);
if (polymorphism && polymorphism.filter) {
const subclassDefinition = polymorphism.filter(x => x.name === jsonObject["$type"])[0];
if (subclassDefinition && subclassDefinition.clazz) {
clazz = subclassDefinition.clazz;
}
}
}
Basically, clazz
is the constructor of the type to deserialize the JSON object to and we replace it with the constructor of the derived type.
This code currently has the restriction that it adds the metadata only to the direct base class, to to the whole hierarchy. But this could easily be solved by adjusting the code inside RegisterForPolymorphism
to walk up the tree.