angular: forwardRef breaks at runtime with ES2015
🐞 bug report
Affected Package
The issue is caused by package @angular/coreIs this a regression?
No.Description
Using forwardRef
as described in https://angular.io/api/core/forwardRef while targeting ES2015 results on a Uncaught ReferenceError: Cannot access 'Lock' before initialization
runtime error.
🔬 Minimal Reproduction
- make a new project
ng new forward-ref-project && cd forward-ref-project
- ensure
tsconfig.json
contains"target": "es2015",
- replace the contents of
src/main.ts
with:
import { Inject, forwardRef, ReflectiveInjector } from '@angular/core';
class Door {
lock: Lock;
// Door attempts to inject Lock, despite it not being defined yet.
// forwardRef makes this possible.
constructor(@Inject(forwardRef(() => Lock)) lock: Lock) { this.lock = lock; }
}
// Only at this point Lock is defined.
class Lock { }
const injector = ReflectiveInjector.resolveAndCreate([Door, Lock]);
const door = injector.get(Door);
console.log(door instanceof Door);
console.log(door.lock instanceof Lock);
ng serve -o
🔥 Exception or Error
Uncaught ReferenceError: Cannot access 'Lock' before initialization
at Module../src/main.ts (main.ts:21)
at __webpack_require__ (bootstrap:78)
at Object.2 (main.ts:30)
at __webpack_require__ (bootstrap:78)
at checkDeferredModules (bootstrap:45)
at Array.webpackJsonpCallback [as push] (bootstrap:32)
at main.js:1
🌍 Your Environment
Angular Version:
Angular CLI: 8.0.0-beta.18
Node: 10.10.0
OS: win32 x64
Angular: 8.0.0-beta.14
... animations, common, compiler, compiler-cli, core, forms
... language-service, platform-browser, platform-browser-dynamic
... router
Package Version
-----------------------------------------------------------
@angular-devkit/architect 0.800.0-beta.18
@angular-devkit/build-angular 0.800.0-beta.18
@angular-devkit/build-optimizer 0.800.0-beta.18
@angular-devkit/build-webpack 0.800.0-beta.18
@angular-devkit/core 8.0.0-beta.18
@angular-devkit/schematics 8.0.0-beta.18
@angular/cli 8.0.0-beta.18
@ngtools/webpack 8.0.0-beta.18
@schematics/angular 8.0.0-beta.18
@schematics/update 0.800.0-beta.18
rxjs 6.4.0
typescript 3.4.5
webpack 4.30.0
Anything else relevant?
forwardRef
is provided specifically for the purpose of referencing something that isn’t defined. This is useful in breaking circular dependencies, and when declaring both services and components in the same file.
forwardRef
works because it delays the resolution of the reference to a time at which it is already declared through the callback indirection. In the API example, the symbol we want to delay resolution is Lock
:
class Door {
lock: Lock;
// Door attempts to inject Lock, despite it not being defined yet.
// forwardRef makes this possible.
constructor(@Inject(forwardRef(() => Lock)) lock: Lock) { this.lock = lock; }
}
// Only at this point Lock is defined.
class Lock { }
But Lock
is actually being referenced in more places than just inside forwardRef
. It is also being used as a TS type in the class property, and in the constructor parameter.
Types don’t usually have a runtime representation so that shouldn’t be a problem. But constructor types are an exception and actually do have a runtime representation. We can see this by looking at the transpiled code:
import * as tslib_1 from "tslib";
import { Inject, forwardRef, ReflectiveInjector } from '@angular/core';
let Door = class Door {
// Door attempts to inject Lock, despite it not being defined yet.
// forwardRef makes this possible.
constructor(lock) { this.lock = lock; }
};
Door = tslib_1.__decorate([
tslib_1.__param(0, Inject(forwardRef(() => Lock))),
tslib_1.__metadata("design:paramtypes", [Lock])
], Door);
// Only at this point Lock is defined.
class Lock {
}
const injector = ReflectiveInjector.resolveAndCreate([Door, Lock]);
const door = injector.get(Door);
console.log(door instanceof Door);
console.log(door.lock instanceof Lock);
The Lock
type in the for the constructor parameter was transpiled into tslib_1.__metadata("design:paramtypes", [Lock])
. This reference does not have a delayed resolution like the injected forwardRef
and is instead immediately resolved, resulting in Uncaught ReferenceError: Cannot access 'Lock' before initialization
.
This error isn’t observed when targetting ES5 however. We can understand why by looking at the code when transpiled to ES5 :
import * as tslib_1 from "tslib";
import { Inject, forwardRef, ReflectiveInjector } from '@angular/core';
var Door = /** @class */ (function () {
// Door attempts to inject Lock, despite it not being defined yet.
// forwardRef makes this possible.
function Door(lock) {
this.lock = lock;
}
Door = tslib_1.__decorate([
tslib_1.__param(0, Inject(forwardRef(function () { return Lock; }))),
tslib_1.__metadata("design:paramtypes", [Lock])
], Door);
return Door;
}());
// Only at this point Lock is defined.
var Lock = /** @class */ (function () {
function Lock() {
}
return Lock;
}());
var injector = ReflectiveInjector.resolveAndCreate([Door, Lock]);
var door = injector.get(Door);
console.log(door instanceof Door);
console.log(door.lock instanceof Lock);
In ES5 there are no class
declarations, so TS instead uses a var
. One important different between var
and class/let/const
is that the latter are all subject to the Temporal Dead Zone.
In practical terms the TDZ means that using a var
before it is declared resolves to undefined
, but using a class/let/const
instead throws a ReferenceError
. This is the error we are seeing here.
A possible workaround is to not declare the type in the constructor:
// Instead of adding the type in the parameter
constructor(@Inject(forwardRef(() => Lock)) lock: Lock) {
this.lock = lock;
}
// Add it as a cast in the constructor body
constructor(@Inject(forwardRef(() => Lock)) lock) {
this.lock = lock as Lock;
}
This will change the transpiled code and remove the reference, avoiding the ReferenceError
:
Door = tslib_1.__decorate([
tslib_1.__param(0, Inject(forwardRef(() => Lock))),
tslib_1.__metadata("design:paramtypes", [Object])
^^^^^^ was Lock before
], Door);
One important note is that the ReferenceError
does not come up on Angular CLI projects compiled with AOT. This is because there we actually transform transpiled TS code and remove Angular decorators, so the metadata reference (tslib_1.__metadata("design:paramtypes", [Lock])
) never reaches the browser and thus there is no ReferenceError
.
About this issue
- Original URL
- State: closed
- Created 5 years ago
- Reactions: 23
- Comments: 19 (7 by maintainers)
Commits related to this issue
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015 output a... — committed to devversion/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties (#37382) In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015... — committed to angular/angular by devversion 4 years ago
- fix(compiler-cli): downlevel angular decorators to static properties (#37382) In v7 of Angular we removed `tsickle` from the default `ngc` pipeline. This had the negative potential of breaking ES2015... — committed to ngwattcos/angular by devversion 4 years ago
+1
I think (in v8) at runtime, a lot of people may be suprised targeting es2015 by default due to the fact CLI historically allowed
"showCircularDependencies": false
and consider circular deps as warning.This will breaks a lot of existing angular project, for sure
I had to separate some classes into their own files to prevent this from happening.
Thanks @istiti , good idea, I’ll wait for now, see if the issue gets resolved during the coming weeks. If not we’ll weigh the investment of time. Anyway, crazy coincidence but seems that we have mutual connections. (we’re in Genèva now for the GIAC)
@filipesilva Hey, sorry for the late answer, busy times. A repro will not be possible for the moment, sorry.
Certainly also because my guess is that it’s related to some circular dependency somewhere and we have a lot of them. (the blocking ones we solve by combinations of forwardRef and using Injector.get()) So a repro would have to include a lot of code from the project is my guess.
If I solve the first error, by changing the code in AppReadyEvent service, then the issue appears in a component. So I actually guess that’s progress 😉. And, now it seems to not be related to forwardRef:
Ha, I’m updating this as I go along 😃, So I was thinking that it could be related to the fact that I give the SheetViewComponent as type, in a component that SheetViewComponent is connected to already. (so they are circularely referencing each other through multiple other components in between). If I change to: sheetViewComponent: any instead of sheetViewComponent: SheetViewComponent it works.
So the change from 7 to 8 seems to be that somehow inputs cannot have circular references?
This sounds like something that Angular simply cannot fix since it’s a limitation of how JS works, right?