Description
TypeScript Version: 4.1.0-dev.20201022
Search Terms: emitDecoratorMetadata, decorators, design:type, type, resolver, reflection
Code:
Suppose we are doing Node.js code and have a plain old circular dependency:
classOne.ts
import { property } from './utils';
import ClassTwo from './classTwo';
export default class ClassOne {
@property
property: ClassTwo;
}
classTwo.ts
import { property } from './utils';
import ClassOne from './classOne';
export default class ClassTwo {
@property
property: ClassOne;
}
and suppose we want to bring a bit of reflection in our code:
index.ts
import 'reflect-metadata';
import { getDesignType } from './utils';
import ClassOne from './classOne';
import ClassTwo from './classTwo';
console.log(`Hello World! ${ClassOne.name} and ${ClassTwo.name};)`);
console.log(`--`);
console.log(`${ClassOne.name} property is of type ${getDesignType(ClassOne, 'property')}`);
console.log(`${ClassTwo.name} property is of type ${getDesignType(ClassTwo, 'property')}`);
utils.ts
/**
* Use typescript emited metadata to get the type
*
* @param ctor
* @param key
*/
export const getDesignType = (ctor: Function, key: string) => {
const type = Reflect.getMetadata('design:type', ctor.prototype, key);
return type?.name;
}
/**
* Dummy decorator
*
* @param target
* @param key
*/
export const property = (target: any, key: string) => { }
Expected behavior:
foo@bar:~$ node ./dist
Hello World! ClassOne and ClassTwo;)
--
ClassOne property is of type ClassTwo
ClassTwo property is of type ClassOne
Actual behavior:
foo@bar:~$ node ./dist
Hello World! ClassOne and ClassTwo;)
--
ClassOne property is of type ClassTwo
ClassTwo property is of type undefined
Explanation:
Well it's not much a surprise, we are running into a circular import issue:
- index.ts is importing ClassOne
- ClassOne is importing ClassTwo
- ClassTwo is importing ClassOne
- Since ClassOne is being read, return the cached value of ClassOne
__metadata("design:type", classOne_1.default)
is evaluatedclassOne_1.default
is evaluated to undefined- ...
Workaround:
There is a simple workaround and it's already used by many libraries but unfortunately not by typescript. The idea is not to emit the type but to emit a type resolver ie. instead of emitting ClassFoo
one should emit () => ClassFoo
. This way we can evaluate the type resolver in index.ts at a time where ClassOne import is already done.
Playground Link:
https://github.com/paeolo/typescript-emit-resolver
Related Issues:
This issue is the root of many issues. Solving this will enable a much better code where reflection is used by not introducing the type resolver as a redondancy.
Please note that trying to solve the circular import is not a good solution. Sometimes circular import are inherents and it's very hard to both focus on code and trying to solve every "circular import" related problems. tslib would benefit a lot from being more resilient to such issues.