Consider the sample of inter-dependent code (below) that makes use of decorators.
Now consider the following workflow (yes, I do want to pass the actual exported classes since I need to use them later):
- App imports and runs
Parent.ts
@Test(Child)
causes the app to importChild.ts
while decorating- Note: the class
Parent
has not yet been reached by the code - In
Child.ts
, the@Test(Parent)
decorator is executed - At this point,
Parent
is undefined and cannot be passed to the decorator.
As you see, there's a nasty circular dependency going on and I cannot see a way of being able to apply decorators that take classes as arguments which reference each other.
Please note, I used @Test
for brevity as an example. The actual decorators are @HasMany
and @BelongsTo
- so I do have an actual use case here.
My question to you: "Is there a solution to this issue?"
My fear is that there isn't, unless TypeScript's piled code is changed so as to defer the decoration process until all involved code has been imported.
Code example:
Decorators.ts
:
export function Test(passedClass: Function): Function {
return function (model: Function): void {
console.log(typeof passedClass);
};
}
Parent.ts
:
import {Child} from "./Child";
import {Test} from "./Decorators";
@Test(Child)
export class Parent {
}
Child.ts
:
import {Parent} from "./Parent";
import {Test} from "./Decorators";
@Test(Parent)
export class Child {
}
Consider the sample of inter-dependent code (below) that makes use of decorators.
Now consider the following workflow (yes, I do want to pass the actual exported classes since I need to use them later):
- App imports and runs
Parent.ts
@Test(Child)
causes the app to importChild.ts
while decorating- Note: the class
Parent
has not yet been reached by the code - In
Child.ts
, the@Test(Parent)
decorator is executed - At this point,
Parent
is undefined and cannot be passed to the decorator.
As you see, there's a nasty circular dependency going on and I cannot see a way of being able to apply decorators that take classes as arguments which reference each other.
Please note, I used @Test
for brevity as an example. The actual decorators are @HasMany
and @BelongsTo
- so I do have an actual use case here.
My question to you: "Is there a solution to this issue?"
My fear is that there isn't, unless TypeScript's piled code is changed so as to defer the decoration process until all involved code has been imported.
Code example:
Decorators.ts
:
export function Test(passedClass: Function): Function {
return function (model: Function): void {
console.log(typeof passedClass);
};
}
Parent.ts
:
import {Child} from "./Child";
import {Test} from "./Decorators";
@Test(Child)
export class Parent {
}
Child.ts
:
import {Parent} from "./Parent";
import {Test} from "./Decorators";
@Test(Parent)
export class Child {
}
Share
edited Sep 5, 2016 at 17:52
JDR
asked Sep 5, 2016 at 16:40
JDRJDR
1,1441 gold badge13 silver badges33 bronze badges
2
- It's not recursion as it's a circular dependencies. Why can't each class test itself? – Nitzan Tomer Commented Sep 5, 2016 at 17:05
-
Thanks - circular reference - that's the term I was grasping for. I named it
@Test
for brevity. It's actually@HasMany
and@BelongsTo
. – JDR Commented Sep 5, 2016 at 17:34
3 Answers
Reset to default 6Hit the same problem today. I solved it slightly differently, by replacing @Test(Parent)
by @Test(() => Parent)
.
Instead of tracking the class constructor (Parent
) in metadata, I track the thunk that returns the constructor (() => Parent
). This delays the evaluation of the Parent
imported variable until the thunk is invoked, which does the trick.
If you can postpone actions performed in decorator - you can overe the limitation and break circular dependency. From the names of your real decorators @HasMany and @BelongsTo it looks like you are attaching some sort of the metadata to each class for the later usage - if so here is my suggestion:
- Extend your @Test decorator signature
export function Test(passedClass: Function | string)
I assume here that the decorator will store meta information in some sort of static dictionary, like: . Where properties can look like
{
hasMany: {new(): any} | string
belongsTo: {new(): any} | string
}
Inside decorator create new properties object with hasMany/belongsTo properties set to
passedClass
. IfpassedClass
is not string - check all already added properties and replace any hasMany/belongsTo that are of string type and equal currentpassedClass.name
Remove reference to Child from Parent.
This is somewhat naive implementation and you can implement some private fields instead to hide the intermediate string data and keep away from exposing union type fields.
Hope this will help you.
How about doing the same, but structuring your code differently?
If both Child
and Parent
reside in the same file then it shouldn't be a problem.
That might not sound optimal as it's fortable to separate code to modules due to length and logic, but that can be solved in a way.
You can have a main file that has base classes for those, even abstract:
// Base.ts
import {Test} from "./Decorators";
@Test(BaseChild)
export abstract class BaseParent {}
@Test(BaseParent)
export abstract class BaseChild {}
And then in your specific modules:
// Parent.ts
import {BaseParent} from "./Base";
export class Parent extends BaseParent {}
And
// Child.ts
import {BaseChild} from "./Base";
export class Child extends BaseChild {}