Skip to content

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

Closed
pleerock opened this issue Aug 28, 2015 · 11 comments
Labels
By Design Deprecated - use "Working as Intended" or "Design Limitation" instead

Comments

@pleerock
Copy link

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#12

You can understand what is problem from my code out run output:

import {Person} from "./Person";
import {Relation} from "./Relation";

export class PersonInfo {
    @Relation(Person)
    info: string;
}
import {Relation} from "./Relation";
import {PersonInfo} from "./PersonInfo";

export class Person {
    name: string;
    @Relation(PersonInfo)
    info: string;
}
export function Relation(cls) {
    return function () {
        console.log(cls);
    }
}
import {Person} from "./Person";
let person = new Person();

result:

undefined
[Function: PersonInfo]

expected result:

[Function: Person]
[Function: PersonInfo]

From generated sources I found that Person_1.Person is undefined here:

    __decorate([
        Relation_1.Relation(Person_1.Person), 
        __metadata('design:type', String)
    ], PersonInfo.prototype, "info");
@weswigham
Copy link
Member

@rbuckton

@rbuckton
Copy link
Contributor

We've discussed this issue amongst the team in the past. We may need to change our __metadata helper to supply a function that when executed would return the metadata instead. This would help somewhat with issues with load order.

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.

@weswigham
Copy link
Member

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 .execute part of the system module is getting executed before all of its dependency slots have finished loading because the dependency is circular. Since decorators are eager, there's no time for it to finish defining itself and let the other module load so it's defined for the decorator call.

@rbuckton
Copy link
Contributor

@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 decorator would need to take a function instead:

// 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

@Verdier
Copy link

Verdier commented Nov 16, 2015

@rbuckton is there any news on that? Looks like the issue is closed but I've not found any other issue to track this. Does the By Design label from @mhegazy means that the problem will never be solved as you suggest in your comments?

@meirgottlieb
Copy link

@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.

@gusev-p
Copy link

gusev-p commented May 9, 2016

I experienced this same issue. @rbuckton, why compiler couldn't delay call to __decorate/__metadata untill all classes are defined? Something like

var A = (function () {
    function A() {
    }

    return A;
}());
var B = (function () {
    function B() {
    }
    return B;
}());
__decorate([
        json, 
        __metadata('design:type', B)
    ], A.prototype, "b", void 0);

instead of

var A = (function () {
    function A() {
    }
    __decorate([
        json, 
        __metadata('design:type', B)
    ], A.prototype, "b", void 0);
    return A;
}());
var B = (function () {
    function B() {
    }
    return B;
}());

@pleerock
Copy link
Author

pleerock commented Jul 6, 2016

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 fowardRef function

@pie6k
Copy link

pie6k commented Oct 29, 2017

Any update on this?

class Car {
  @Field() owner: Person;
}
class Person {
  @Field() car: Car;
}

this is still returning error. ReferenceError: Person is not defined

@edwinquaihoi
Copy link

Any update on this issue, we are facing this issue when using the jsog-typescript library.

@Strate
Copy link

Strate commented Apr 27, 2018

I fixed this issue with custom babel plugin, which transforms all calls to tslib.__metadata to make them lazy:

before:

tslib.__metadata("design:type", SomeClass)

after:

tslib.__metadata("design:type", Reflect.asFactory(() => SomeClass))

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 Reflect.getMetadata call

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
                                });
                        }
                    }
                }
            }
        }
    };
}

@microsoft microsoft locked and limited conversation to collaborators Jul 31, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
By Design Deprecated - use "Working as Intended" or "Design Limitation" instead
Projects
None yet
Development

No branches or pull requests

10 participants