-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Classes passed to decorators dont work when there circular references used in those classes #4521
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
We've discussed this issue amongst the team in the past. We may need to change our If we decide to go with that solution, the output from your example in microsoft/reflect-metadata#12 would instead look as follows: __decorate([
Relation_1.Relation(),
__metadata('design:type', function() { return Person_1.Person; })
], PersonInfo.prototype, "info"); Then your call to Reflect.getMetadata would change to something like: let typefn = Reflect.getMetadata('design:type', object, propertyName);
let type = typefn(); You still wouldn't be able to get the 'design:type' if the constructor from the external file has still not yet been exported, but there is no way around that given the execution semantics of JavaScript. |
In this case, the issue is with Relation_1.Relation(Person_1.Person) //Person_1.Person is undefined not __metadata('design:type', String) //This is perfectly fine Metadata is immaterial to the issue - the decorator is referencing what should be a resolved import as an argument, but at decoration time it is not yet resolved, for some reason. What's going on is that the |
@weswigham This is again due to execution order. If you look at a simpler example: var a = function(b) { console.log(typeof b); }
a(b); // "undefined";
var b = function(a) { console.log(typeof a); }
b(a); // "function"; Decorators do not change the fact that while the BindingIdentifier for a VariableDeclaration or ClassDeclaration are hoisted, the initializers or class bodies are not evaluated until execution proceeds to the point where they are declared. Imports and exports don't really change this underlying behavior, so circular references behave in the same fashion. In fact, the only declaration that hoists the body of the declaration is a FunctionDeclaration. As such, the same example above written using function declarations has different output: function a(b) { console.log(typeof b); }
a(b); // "function";
function b(a) { console.log(typeof a); }
b(a); // "function"; Since _ClassDeclaration_s and _VariableDeclaration_s don't hoist their bodies or initializers, the only way to handle this out of order is to enclose the references in a function closure that can be evaluated later, after all class bodies or initializers (or exports in a circular module dependency graph) have been evaluated. As such, @pleerock's // Relation.ts
let relationMap = new WeakMap<any, Map<string, () => Function>>();
export function Relation(typefn: () => Function) {
return function (target, propertyName) {
// Calling `typefn` here may not give you the value, you need to
// store it somewhere and evaluate it later...
let map = relationMap.get(target);
if (!map) relationMap.set(target, map = new Map<string, () => Function>());
map.set(propertyName, typefn);
}
}
export function getRelation(target, propertyName) {
while (target) {
let map = relationMap.get(target);
if (map) {
let typefn = map.get(propertyName);
if (typefn) {
return typefn();
}
}
target = Object.getPrototypeOf(target);
}
} // PersonInfo.ts
import { Person } from './Person';
import { Relation } from './Relation';
export class PersonInfo {
@Relation(() => Person)
info: string;
} // Person.ts
import { PersonInfo } from './PersonInfo';
import { Relation } from './Relation';
export class Person {
@Relation(() => PersonInfo)
info: string;
} // App.ts
import { Person } from './Person';
import { PersonInfo } from './PersonInfo';
import { getRelation } from './Relation';
var a = new Person();
var b = new PersonInfo();
getRelation(a, "info"); // PersonInfo
getRelation(b, "info"); // Person |
@rbuckton, I understand this issue is closed, but I just wanted to point out that this same problem exists with the type information provided by the emitDecoratorMetadata compiler flag. This is a slightly different problem because it can't be fixed by the user passing a factory function to the decorator since the code is generated by the compiler. |
I experienced this same issue. @rbuckton, why compiler couldn't delay call to __decorate/__metadata untill all classes are defined? Something like
instead of
|
currently I think its one of the biggest design issue we have. We all are forced to use monkey patching like functions that returns types, or for example angular team uses |
Any update on this?
this is still returning error. |
Any update on this issue, we are facing this issue when using the jsog-typescript library. |
I fixed this issue with custom babel plugin, which transforms all calls to before: tslib.__metadata("design:type", SomeClass) after:
Also I made a monkey-patch for Reflect object (from reflect-metadata package), which add support for lazy-metadata: import "reflect-metadata"
declare global {
namespace Reflect {
function asFactory(factory: () => any): any;
}
}
interface LazyReflectFactory {
(): any
__lazyFactory: true
}
function isLazyReflectFactory(arg: any): arg is LazyReflectFactory {
return typeof arg === "function" && arg.length === 0 && arg.__lazyFactory === true
}
Reflect.asFactory = (factory) => Object.assign(factory, {__lazyFactory: true})
Reflect.getMetadata = (
(originalMetadata: typeof Reflect.getMetadata) =>
(...args: any[]) => {
let result = originalMetadata.apply(Reflect, args)
if (isLazyReflectFactory(result)) {
result = result()
}
return result
}
)(Reflect.getMetadata) So, actual metadata resolving deferred to Code for babel plugin here: module.exports = function(babel) {
const { types: t } = babel;
const decorateVisitor = {
CallExpression(path) {
if (
t.isMemberExpression(path.node.callee) &&
t.isIdentifier(path.node.callee.object, {name: this.tslibAlias}) &&
t.isIdentifier(path.node.callee.property, {name: "__metadata"}) &&
path.node.arguments.length === 2 &&
t.isStringLiteral(path.node.arguments[0])
) {
path.get("arguments")[1].replaceWith(
t.callExpression(
t.memberExpression(t.Identifier("Reflect"), t.Identifier("asFactory")),
[t.arrowFunctionExpression([], path.node.arguments[1])]
)
)
}
}
};
return {
name: "transform-metadata-to-lazy", // not required
visitor: {
ImportDeclaration(path) {
if (path.node.source.value === "tslib") {
for (const spec of path.node.specifiers) {
if (t.isImportNamespaceSpecifier(spec)) {
path
.getFunctionParent()
.traverse(decorateVisitor, {
tslibAlias: spec.local.name
});
}
}
}
}
}
};
} |
First I thought the problem in
reflect-metadata
component I used, but now I think the issue is with typescript decorators themself. I described that issue there: microsoft/reflect-metadata#12You can understand what is problem from my code out run output:
result:
expected result:
From generated sources I found that Person_1.Person is undefined here:
The text was updated successfully, but these errors were encountered: