Typescript: Classes passed to decorators dont work when there circular references used in those classes

Created on 28 Aug 2015  路  11Comments  路  Source: microsoft/TypeScript

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: https://github.com/rbuckton/ReflectDecorators/issues/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:

[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");
By Design

Most helpful comment

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

All 11 comments

@rbuckton

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 rbuckton/ReflectDecorators#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 .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.

@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

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

@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

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

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

Any update on this?

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

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

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 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
                                });
                        }
                    }
                }
            }
        }
    };
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

dlaberge picture dlaberge  路  3Comments

kyasbal-1994 picture kyasbal-1994  路  3Comments

seanzer picture seanzer  路  3Comments

Antony-Jones picture Antony-Jones  路  3Comments

weswigham picture weswigham  路  3Comments