Reflection in Typescript
Check reflection state in Typescript
The reflection we are used to.
Languages like C#, Java, PHP have a reflection mechanism.
Reflection is a feature in the Java programming language. It allows an executing Java program to examine or “introspect” upon itself, and manipulate internal properties of the program. For example, it’s possible for a Java class to obtain the names of all its members and display them.
Reflect has been introduced in Javascript. But that’s not what we’re used to in other languages. Most of the features have been available via “proxy handler” or raw Object. Reflect functionality is quite poor. Javascript doesn’t have a strong type system, interfaces, abstract/final classes so you cannot
- get metadata about argument or types
- define if a class implements a certain interface
- define if a class is abstract
- define if a method is public/private
- etc
Basically we can get all information about a class using Reflection.
But what about typescript?
Typescript has classes, interfaces, visibility, and strict types. So potentially we could get meta-information about it during TS runtime. Unfortunately, it’s not so easy… and frankly, it’s not possible. Typescript code, in the end, will be transformed into Javascript.
interface Bar {
buzz: (a: string) => string;
}
class Foo implements Bar {
public buzz(a: string): string {
return '';
}
static b: string = '';
}
Will be translated to
var Foo = (function () {
function Foo() {}
Foo.prototype.buzz = function (a) {
return '';
};
Foo.b = '';
return Foo;
}());
As you can see JS version doesn’t know anything about Bar
interface, argument types don’t exist. All reflection features are not available at JS runtime.
But how does reflection work in Typescript…
Typescript has extended reflect-metadata it allows you to get function argument and return types in conjunction with emitDecoratorMetadata. Let’s looks at how it works. We will need to enable
"experimentalDecorators": true,
"emitDecoratorMetadata": true
Now if we add some ourCustomDecorator
class Foo {
@ourCustomDecorator
public buzz(a: string): string {
return '';
}
}
Our javascript will be translated to
var __decorate = (...
var __metadata = (...__decorate([
ourCustomDecorator,
__metadata("design:type", Function),
__metadata("design:paramtypes", [String]),
__metadata("design:returntype", String)
], Foo.prototype, "buzz", null);
Did you expect some magic?
Now you can get metadata info
console.log(Reflect.getMetadata("design:type", target, propertyKey));
// [Function: Function]
// Checks the types of all params
console.log(Reflect.getMetadata("design:paramtypes", target, propertyKey));
// [[Function: Number]]
// Checks the return type
console.log(Reflect.getMetadata("design:returntype", target, propertyKey));
You can find more docs on typescript site and we are going to discuss the approach here.
As you can see emitDecoratorMetadata
will add metadata in inline style to Reflect
store/object and it’s very straightforward. But this approach is quite poor and robs us of most of Reflect functionality that we getting used to in other languages.
Pros
We can get paramtypes
info for the constructor and methods. That feature is widely used in DI libraries like tsyringe
and inversify
@injectable()
class Foo {
constructor(private database: Database) {}
}
When you decorate a class — you can get constructor metadata.
Cons
- decorators and reflection are mixed together. It’s quite different features but the current approach doesn’t allow using metadata without decorators, that are still experimental BTW.
- We deal with a target, but not with a class — Reflect.getMetadata(“design:type”, target, propertyKey). It’s not convenient since the target can be not available.
- In order to get metadata for your method, you need to wrap it with a
@decorator
On the other hand, if you don’t need meta but need a decorator — still metadata will be injected. Basically, __metadata will be injected into the final JS code even if it does not really need it. - Missing other Reflection features described above — visibility, access to class methods, constants, etc.
Summary
The reality is that Typescript Reflection is very poor. In order to get a very basic set of Reflection features — we need to make a number of hacky solutions. Other than Dependency Injection tools, I see no other use for this limited functionality. And remember that there’s an awesome DI package that doesn’t require decorators and metadata.
In the end, I ask myself — should I use decorators and metadata at Typescript? And for now, I answer NO. Please comment — do you know other examples when TS Reflection is really useful.